mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-24 22:55:21 -04:00
New: Bulk Manage Applications, Download Clients
Co-authored-by: Qstick <qstick@gmail.com>
This commit is contained in:
@@ -190,7 +190,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
|
||||
return (
|
||||
<SelectProvider items={items}>
|
||||
<PageContent>
|
||||
<PageContent title={translate('Indexers')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
|
||||
@@ -7,7 +7,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
|
||||
import { bulkDeleteIndexers } from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteIndexerModalContent.css';
|
||||
@@ -34,7 +34,7 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
const onDeleteIndexerConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
bulkDeleteIndexers({
|
||||
indexerIds,
|
||||
ids: indexerIds,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerModalContent.css';
|
||||
|
||||
@@ -15,6 +15,10 @@ interface SavePayload {
|
||||
enable?: boolean;
|
||||
appProfileId?: number;
|
||||
priority?: number;
|
||||
minimumSeeders?: number;
|
||||
seedRatio?: number;
|
||||
seedTime?: number;
|
||||
packSeedTime?: number;
|
||||
}
|
||||
|
||||
interface EditIndexerModalContentProps {
|
||||
@@ -37,6 +41,14 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
const [enable, setEnable] = useState(NO_CHANGE);
|
||||
const [appProfileId, setAppProfileId] = useState<string | number>(NO_CHANGE);
|
||||
const [priority, setPriority] = useState<null | string | number>(null);
|
||||
const [minimumSeeders, setMinimumSeeders] = useState<null | string | number>(
|
||||
null
|
||||
);
|
||||
const [seedRatio, setSeedRatio] = useState<null | string | number>(null);
|
||||
const [seedTime, setSeedTime] = useState<null | string | number>(null);
|
||||
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
|
||||
null
|
||||
);
|
||||
|
||||
const save = useCallback(() => {
|
||||
let hasChanges = false;
|
||||
@@ -57,12 +69,42 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
payload.priority = priority as number;
|
||||
}
|
||||
|
||||
if (minimumSeeders !== null) {
|
||||
hasChanges = true;
|
||||
payload.minimumSeeders = minimumSeeders as number;
|
||||
}
|
||||
|
||||
if (seedRatio !== null) {
|
||||
hasChanges = true;
|
||||
payload.seedRatio = seedRatio as number;
|
||||
}
|
||||
|
||||
if (seedTime !== null) {
|
||||
hasChanges = true;
|
||||
payload.seedTime = seedTime as number;
|
||||
}
|
||||
|
||||
if (packSeedTime !== null) {
|
||||
hasChanges = true;
|
||||
payload.packSeedTime = packSeedTime as number;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
onSavePress(payload);
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
}, [enable, appProfileId, priority, onSavePress, onModalClose]);
|
||||
}, [
|
||||
enable,
|
||||
appProfileId,
|
||||
priority,
|
||||
minimumSeeders,
|
||||
seedRatio,
|
||||
seedTime,
|
||||
packSeedTime,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }) => {
|
||||
@@ -76,6 +118,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
case 'priority':
|
||||
setPriority(value);
|
||||
break;
|
||||
case 'minimumSeeders':
|
||||
setMinimumSeeders(value);
|
||||
break;
|
||||
case 'seedRatio':
|
||||
setSeedRatio(value);
|
||||
break;
|
||||
case 'seedTime':
|
||||
setSeedTime(value);
|
||||
break;
|
||||
case 'packSeedTime':
|
||||
setPackSeedTime(value);
|
||||
break;
|
||||
default:
|
||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||
}
|
||||
@@ -94,7 +148,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
<ModalHeader>{translate('EditSelectedIndexers')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -106,21 +160,22 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('SyncProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.APP_PROFILE_SELECT}
|
||||
name="appProfileId"
|
||||
value={appProfileId}
|
||||
helpText={translate('AppProfileSelectHelpText')}
|
||||
includeNoChange={true}
|
||||
includeNoChangeDisabled={false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Priority')}</FormLabel>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('IndexerPriority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
@@ -128,6 +183,57 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
value={priority}
|
||||
min={1}
|
||||
max={50}
|
||||
helpText={translate('IndexerPriorityHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('AppsMinimumSeeders')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="minimumSeeders"
|
||||
value={minimumSeeders}
|
||||
helpText={translate('AppsMinimumSeedersHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('SeedRatio')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="seedRatio"
|
||||
value={seedRatio}
|
||||
helpText={translate('SeedRatioHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('SeedTime')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="seedTime"
|
||||
value={seedTime}
|
||||
unit={translate('minutes')}
|
||||
helpText={translate('SeedTimeHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('PackSeedTime')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="packSeedTime"
|
||||
value={packSeedTime}
|
||||
unit={translate('minutes')}
|
||||
helpText={translate('PackSeedTimeHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -2,11 +2,12 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
|
||||
import { bulkEditIndexers } from 'Store/Actions/indexerActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import DeleteIndexerModal from './Delete/DeleteIndexerModal';
|
||||
@@ -15,7 +16,7 @@ import TagsModal from './Tags/TagsModal';
|
||||
import styles from './IndexerIndexSelectFooter.css';
|
||||
|
||||
const indexersEditorSelector = createSelector(
|
||||
(state) => state.indexers,
|
||||
(state: AppState) => state.indexers,
|
||||
(indexers) => {
|
||||
const { isSaving, isDeleting, deleteError } = indexers;
|
||||
|
||||
@@ -64,9 +65,9 @@ function IndexerIndexSelectFooter() {
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveIndexerEditor({
|
||||
bulkEditIndexers({
|
||||
...payload,
|
||||
indexerIds,
|
||||
ids: indexerIds,
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -87,8 +88,8 @@ function IndexerIndexSelectFooter() {
|
||||
setIsTagsModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveIndexerEditor({
|
||||
indexerIds,
|
||||
bulkEditIndexers({
|
||||
ids: indexerIds,
|
||||
tags,
|
||||
applyTags,
|
||||
})
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
|
||||
.priority,
|
||||
.protocol,
|
||||
.privacy {
|
||||
.privacy,
|
||||
.minimumSeeders,
|
||||
.seedRatio,
|
||||
.seedTime,
|
||||
.packSeedTime {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 90px;
|
||||
|
||||
@@ -8,9 +8,13 @@ interface CssExports {
|
||||
'cell': string;
|
||||
'checkInput': string;
|
||||
'externalLink': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'priority': string;
|
||||
'privacy': string;
|
||||
'protocol': string;
|
||||
'seedRatio': string;
|
||||
'seedTime': string;
|
||||
'sortName': string;
|
||||
'status': string;
|
||||
'tags': string;
|
||||
|
||||
@@ -55,6 +55,23 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
const vipExpiration =
|
||||
fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
|
||||
|
||||
const minimumSeeders =
|
||||
fields.find(
|
||||
(field) => field.name === 'torrentBaseSettings.appMinimumSeeders'
|
||||
)?.value ?? undefined;
|
||||
|
||||
const seedRatio =
|
||||
fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')
|
||||
?.value ?? undefined;
|
||||
|
||||
const seedTime =
|
||||
fields.find((field) => field.name === 'torrentBaseSettings.seedTime')
|
||||
?.value ?? undefined;
|
||||
|
||||
const packSeedTime =
|
||||
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
|
||||
?.value ?? undefined;
|
||||
|
||||
const rssUrl = `${window.location.origin}${
|
||||
window.Prowlarr.urlBase
|
||||
}/${id}/api?apikey=${encodeURIComponent(
|
||||
@@ -213,6 +230,38 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'minimumSeeders') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{minimumSeeders}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'seedRatio') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{seedRatio}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'seedTime') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{seedTime}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'packSeedTime') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{packSeedTime}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
|
||||
.priority,
|
||||
.privacy,
|
||||
.protocol {
|
||||
.protocol,
|
||||
.minimumSeeders,
|
||||
.seedRatio,
|
||||
.seedTime,
|
||||
.packSeedTime {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 90px;
|
||||
|
||||
@@ -5,9 +5,13 @@ interface CssExports {
|
||||
'added': string;
|
||||
'appProfileId': string;
|
||||
'capabilities': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'priority': string;
|
||||
'privacy': string;
|
||||
'protocol': string;
|
||||
'seedRatio': string;
|
||||
'seedTime': string;
|
||||
'sortName': string;
|
||||
'status': string;
|
||||
'tags': string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
setIndexerSort,
|
||||
setIndexerTableOption,
|
||||
} from 'Store/Actions/indexerIndexActions';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import IndexerIndexTableOptions from './IndexerIndexTableOptions';
|
||||
import styles from './IndexerIndexTableHeader.css';
|
||||
|
||||
@@ -45,7 +46,7 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
|
||||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: SelectStateInputProps) => {
|
||||
selectDispatch({
|
||||
type: value ? 'selectAll' : 'unselectAll',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user