diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css
index bf31501dd..e0f1bf5dc 100644
--- a/frontend/src/Content/Fonts/fonts.css
+++ b/frontend/src/Content/Fonts/fonts.css
@@ -25,14 +25,3 @@
font-family: 'Ubuntu Mono';
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
}
-
-/*
- * text-security-disc
- */
-
-@font-face {
- font-weight: normal;
- font-style: normal;
- font-family: 'text-security-disc';
- src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
-}
diff --git a/frontend/src/Content/Fonts/text-security-disc.ttf b/frontend/src/Content/Fonts/text-security-disc.ttf
deleted file mode 100644
index 86038dba8..000000000
Binary files a/frontend/src/Content/Fonts/text-security-disc.ttf and /dev/null differ
diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff
deleted file mode 100644
index bc4cc324b..000000000
Binary files a/frontend/src/Content/Fonts/text-security-disc.woff and /dev/null differ
diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts
new file mode 100644
index 000000000..417db8178
--- /dev/null
+++ b/frontend/src/DownloadClient/DownloadProtocol.ts
@@ -0,0 +1,3 @@
+type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
+
+export default DownloadProtocol;
diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts
new file mode 100644
index 000000000..3caf66df2
--- /dev/null
+++ b/frontend/src/Helpers/Hooks/useCurrentPage.ts
@@ -0,0 +1,9 @@
+import { useHistory } from 'react-router-dom';
+
+function useCurrentPage() {
+ const history = useHistory();
+
+ return history.action === 'POP';
+}
+
+export default useCurrentPage;
diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts
new file mode 100644
index 000000000..24cffb2f1
--- /dev/null
+++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts
@@ -0,0 +1,17 @@
+import { useCallback, useState } from 'react';
+
+export default function useModalOpenState(
+ initialState: boolean
+): [boolean, () => void, () => void] {
+ const [isOpen, setIsOpen] = useState(initialState);
+
+ const setModalOpen = useCallback(() => {
+ setIsOpen(true);
+ }, [setIsOpen]);
+
+ const setModalClosed = useCallback(() => {
+ setIsOpen(false);
+ }, [setIsOpen]);
+
+ return [isOpen, setModalOpen, setModalClosed];
+}
diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts
new file mode 100644
index 000000000..885c73470
--- /dev/null
+++ b/frontend/src/Helpers/Props/TooltipPosition.ts
@@ -0,0 +1,3 @@
+type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
+
+export default TooltipPosition;
diff --git a/frontend/src/Helpers/Props/align.js b/frontend/src/Helpers/Props/align.ts
similarity index 100%
rename from frontend/src/Helpers/Props/align.js
rename to frontend/src/Helpers/Props/align.ts
diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js
index 7fed535f2..73ef41956 100644
--- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js
+++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js
@@ -2,9 +2,10 @@ export const BOOL = 'bool';
export const BYTES = 'bytes';
export const DATE = 'date';
export const DEFAULT = 'default';
+export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer';
export const PROTOCOL = 'protocol';
export const PRIVACY = 'privacy';
export const APP_PROFILE = 'appProfile';
-export const MOVIE_STATUS = 'movieStatus';
+export const CATEGORY = 'category';
export const TAG = 'tag';
diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js
index 834452242..773748996 100644
--- a/frontend/src/Helpers/Props/icons.js
+++ b/frontend/src/Helpers/Props/icons.js
@@ -43,6 +43,7 @@ import {
faChevronCircleRight as fasChevronCircleRight,
faChevronCircleUp as fasChevronCircleUp,
faCircle as fasCircle,
+ faCircleDown as fasCircleDown,
faCloud as fasCloud,
faCloudDownloadAlt as fasCloudDownloadAlt,
faCog as fasCog,
@@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
export const CHECK_CIRCLE = fasCheckCircle;
export const CHECK_SQUARE = fasSquareCheck;
export const CIRCLE = fasCircle;
+export const CIRCLE_DOWN = fasCircleDown;
export const CIRCLE_OUTLINE = farCircle;
export const CLEAR = fasTrashAlt;
export const CLIPBOARD = fasCopy;
diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js
index 6c4564341..f9cd58e6d 100644
--- a/frontend/src/Helpers/Props/inputTypes.js
+++ b/frontend/src/Helpers/Props/inputTypes.js
@@ -1,6 +1,5 @@
export const AUTO_COMPLETE = 'autoComplete';
export const APP_PROFILE_SELECT = 'appProfileSelect';
-export const AVAILABILITY_SELECT = 'availabilitySelect';
export const CAPTCHA = 'captcha';
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
export const CHECK = 'check';
@@ -27,7 +26,6 @@ export const TAG_SELECT = 'tagSelect';
export const all = [
AUTO_COMPLETE,
APP_PROFILE_SELECT,
- AVAILABILITY_SELECT,
CAPTCHA,
CARDIGANNCAPTCHA,
CHECK,
diff --git a/frontend/src/Helpers/Props/kinds.js b/frontend/src/Helpers/Props/kinds.ts
similarity index 72%
rename from frontend/src/Helpers/Props/kinds.js
rename to frontend/src/Helpers/Props/kinds.ts
index b0f5ac87f..7ce606716 100644
--- a/frontend/src/Helpers/Props/kinds.js
+++ b/frontend/src/Helpers/Props/kinds.ts
@@ -7,7 +7,6 @@ export const PRIMARY = 'primary';
export const PURPLE = 'purple';
export const SUCCESS = 'success';
export const WARNING = 'warning';
-export const QUEUE = 'queue';
export const all = [
DANGER,
@@ -19,5 +18,15 @@ export const all = [
PURPLE,
SUCCESS,
WARNING,
- QUEUE
-];
+] as const;
+
+export type Kind =
+ | 'danger'
+ | 'default'
+ | 'disabled'
+ | 'info'
+ | 'inverse'
+ | 'primary'
+ | 'purple'
+ | 'success'
+ | 'warning';
diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.ts
similarity index 71%
rename from frontend/src/Helpers/Props/sizes.js
rename to frontend/src/Helpers/Props/sizes.ts
index d7f85df5e..ca7a50fbf 100644
--- a/frontend/src/Helpers/Props/sizes.js
+++ b/frontend/src/Helpers/Props/sizes.ts
@@ -4,4 +4,6 @@ export const MEDIUM = 'medium';
export const LARGE = 'large';
export const EXTRA_LARGE = 'extraLarge';
-export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
+export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const;
+
+export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge';
diff --git a/frontend/src/History/Details/HistoryDetails.js b/frontend/src/History/Details/HistoryDetails.js
index e0ae06eb1..6d5ab260e 100644
--- a/frontend/src/History/Details/HistoryDetails.js
+++ b/frontend/src/History/Details/HistoryDetails.js
@@ -3,6 +3,7 @@ import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Link from 'Components/Link/Link';
+import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
@@ -10,7 +11,10 @@ function HistoryDetails(props) {
const {
indexer,
eventType,
- data
+ date,
+ data,
+ shortDateFormat,
+ timeFormat
} = props;
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
@@ -21,7 +25,10 @@ function HistoryDetails(props) {
limit,
offset,
source,
- url
+ host,
+ url,
+ elapsedTime,
+ cached
} = data;
return (
@@ -86,6 +93,15 @@ function HistoryDetails(props) {
null
}
+ {
+ data ?
+
:
+ null
+ }
+
{
data ?
:
null
}
+
+ {
+ elapsedTime ?
+
:
+ null
+ }
+
+ {
+ date ?
+
:
+ null
+ }
);
}
@@ -101,10 +135,19 @@ function HistoryDetails(props) {
if (eventType === 'releaseGrabbed') {
const {
source,
+ host,
grabTitle,
- url
+ url,
+ publishedDate,
+ infoUrl,
+ downloadClient,
+ downloadClientName,
+ elapsedTime,
+ grabMethod
} = data;
+ const downloadClientNameInfo = downloadClientName ?? downloadClient;
+
return (
{
@@ -125,6 +168,15 @@ function HistoryDetails(props) {
null
}
+ {
+ data ?
+ :
+ null
+ }
+
{
data ?
{infoUrl}}
+ /> :
+ null
+ }
+
+ {
+ publishedDate ?
+ :
+ null
+ }
+
+ {
+ downloadClientNameInfo ?
+ :
+ null
+ }
+
{
data ?
:
null
}
+
+ {
+ elapsedTime ?
+ :
+ null
+ }
+
+ {
+ grabMethod ?
+ :
+ null
+ }
+
+ {
+ date ?
+ :
+ null
+ }
);
}
if (eventType === 'indexerAuth') {
+ const { elapsedTime } = data;
+
return (
:
null
}
+
+ {
+ elapsedTime ?
+
:
+ null
+ }
+
+ {
+ date ?
+
:
+ null
+ }
);
}
@@ -171,6 +297,15 @@ function HistoryDetails(props) {
title={translate('Name')}
data={data.query}
/>
+
+ {
+ date ?
+
:
+ null
+ }
);
}
@@ -178,6 +313,7 @@ function HistoryDetails(props) {
HistoryDetails.propTypes = {
indexer: PropTypes.object.isRequired,
eventType: PropTypes.string.isRequired,
+ date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
diff --git a/frontend/src/History/Details/HistoryDetailsModal.js b/frontend/src/History/Details/HistoryDetailsModal.js
index fbc3114ad..560955de3 100644
--- a/frontend/src/History/Details/HistoryDetailsModal.js
+++ b/frontend/src/History/Details/HistoryDetailsModal.js
@@ -29,6 +29,7 @@ function HistoryDetailsModal(props) {
isOpen,
eventType,
indexer,
+ date,
data,
shortDateFormat,
timeFormat,
@@ -49,6 +50,7 @@ function HistoryDetailsModal(props) {
@@ -193,8 +196,9 @@ History.propTypes = {
indexersError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- selectedFilterKey: PropTypes.string.isRequired,
+ selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired,
diff --git a/frontend/src/History/HistoryConnector.js b/frontend/src/History/HistoryConnector.js
index cd634ca11..9b29b7d6e 100644
--- a/frontend/src/History/HistoryConnector.js
+++ b/frontend/src/History/HistoryConnector.js
@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import * as historyActions from 'Store/Actions/historyActions';
+import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
@@ -14,13 +15,15 @@ function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.indexers,
+ createCustomFiltersSelector('history'),
createCommandExecutingSelector(commandNames.CLEAR_HISTORY),
- (history, indexers, isHistoryClearing) => {
+ (history, indexers, customFilters, isHistoryClearing) => {
return {
isIndexersFetching: indexers.isFetching,
isIndexersPopulated: indexers.isPopulated,
indexersError: indexers.error,
isHistoryClearing,
+ customFilters,
...history
};
}
diff --git a/frontend/src/History/HistoryEventTypeCell.js b/frontend/src/History/HistoryEventTypeCell.js
index b6478eb90..15654d953 100644
--- a/frontend/src/History/HistoryEventTypeCell.js
+++ b/frontend/src/History/HistoryEventTypeCell.js
@@ -20,21 +20,22 @@ function getIconName(eventType) {
}
}
-function getIconKind(successful) {
- switch (successful) {
- case false:
- return kinds.DANGER;
- default:
- return kinds.DEFAULT;
+function getIconKind(successful, redirect) {
+ if (redirect) {
+ return kinds.INFO;
+ } else if (!successful) {
+ return kinds.DANGER;
}
+
+ return kinds.DEFAULT;
}
-function getTooltip(eventType, data, indexer) {
+function getTooltip(eventType, data, indexer, redirect) {
switch (eventType) {
case 'indexerQuery':
return `Query "${data.query}" sent to ${indexer.name}`;
case 'releaseGrabbed':
- return `Release grabbed from ${indexer.name}`;
+ return redirect ? `Release grabbed via redirect from ${indexer.name}` : `Release grabbed from ${indexer.name}`;
case 'indexerAuth':
return `Auth attempted for ${indexer.name}`;
case 'indexerRss':
@@ -45,9 +46,12 @@ function getTooltip(eventType, data, indexer) {
}
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
+ const { grabMethod } = data;
+ const redirect = grabMethod && grabMethod.toLowerCase() === 'redirect';
+
const iconName = getIconName(eventType);
- const iconKind = getIconKind(successful);
- const tooltip = getTooltip(eventType, data, indexer);
+ const iconKind = getIconKind(successful, redirect);
+ const tooltip = getTooltip(eventType, data, indexer, redirect);
return (
state.history.items,
+ (queueItems) => {
+ return queueItems;
+ }
+ );
+}
+
+function createFilterBuilderPropsSelector() {
+ return createSelector(
+ (state: AppState) => state.history.filterBuilderProps,
+ (filterBuilderProps) => {
+ return filterBuilderProps;
+ }
+ );
+}
+
+interface HistoryFilterModalProps {
+ isOpen: boolean;
+}
+
+export default function HistoryFilterModal(props: HistoryFilterModalProps) {
+ const sectionItems = useSelector(createHistorySelector());
+ const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
+ const customFilterType = 'history';
+
+ const dispatch = useDispatch();
+
+ const dispatchSetFilter = useCallback(
+ (payload: unknown) => {
+ dispatch(setHistoryFilter(payload));
+ },
+ [dispatch]
+ );
+
+ return (
+
+ );
+}
diff --git a/frontend/src/History/HistoryRow.js b/frontend/src/History/HistoryRow.js
index f5af8bb11..1d8c38850 100644
--- a/frontend/src/History/HistoryRow.js
+++ b/frontend/src/History/HistoryRow.js
@@ -257,6 +257,7 @@ class HistoryRow extends Component {
key={parameter.key}
title={parameter.title}
value={data[parameter.key]}
+ queryType={data.queryType}
/>
);
}
@@ -331,6 +332,21 @@ class HistoryRow extends Component {
);
}
+ if (name === 'host') {
+ return (
+
+ {
+ data.host ?
+ data.host :
+ null
+ }
+
+ );
+ }
+
if (name === 'elapsedTime') {
return (
);
}
@@ -393,6 +410,7 @@ class HistoryRow extends Component {
{value};
} else if (type === 'tmdb') {
link = (
- {value}
+
+ {value}
+
);
} else if (type === 'tvdb') {
link = (
diff --git a/frontend/src/Indexer/Add/AddIndexerModal.js b/frontend/src/Indexer/Add/AddIndexerModal.js
deleted file mode 100644
index 9344c8130..000000000
--- a/frontend/src/Indexer/Add/AddIndexerModal.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import Modal from 'Components/Modal/Modal';
-import { sizes } from 'Helpers/Props';
-import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
-import styles from './AddIndexerModal.css';
-
-function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
- return (
-
-
-
- );
-}
-
-AddIndexerModal.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- onModalClose: PropTypes.func.isRequired,
- onSelectIndexer: PropTypes.func.isRequired
-};
-
-export default AddIndexerModal;
diff --git a/frontend/src/Indexer/Add/AddIndexerModal.tsx b/frontend/src/Indexer/Add/AddIndexerModal.tsx
new file mode 100644
index 000000000..be22eec57
--- /dev/null
+++ b/frontend/src/Indexer/Add/AddIndexerModal.tsx
@@ -0,0 +1,44 @@
+import React, { useCallback } from 'react';
+import { useDispatch } from 'react-redux';
+import Modal from 'Components/Modal/Modal';
+import { sizes } from 'Helpers/Props';
+import { clearIndexerSchema } from 'Store/Actions/indexerActions';
+import AddIndexerModalContent from './AddIndexerModalContent';
+import styles from './AddIndexerModal.css';
+
+interface AddIndexerModalProps {
+ isOpen: boolean;
+ onSelectIndexer(): void;
+ onModalClose(): void;
+}
+
+function AddIndexerModal({
+ isOpen,
+ onSelectIndexer,
+ onModalClose,
+ ...otherProps
+}: AddIndexerModalProps) {
+ const dispatch = useDispatch();
+
+ const onModalClosePress = useCallback(() => {
+ dispatch(clearIndexerSchema());
+ onModalClose();
+ }, [dispatch, onModalClose]);
+
+ return (
+
+
+
+ );
+}
+
+export default AddIndexerModal;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css b/frontend/src/Indexer/Add/AddIndexerModalContent.css
index a58eccfbc..e824c5475 100644
--- a/frontend/src/Indexer/Add/AddIndexerModalContent.css
+++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css
@@ -19,12 +19,18 @@
margin-bottom: 16px;
}
-.alert {
+.notice {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}
+.alert {
+ composes: alert from '~Components/Alert.css';
+
+ text-align: center;
+}
+
.scroller {
flex: 1 1 auto;
}
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts
index cbedc72a4..5978832e4 100644
--- a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts
+++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts
@@ -10,6 +10,7 @@ interface CssExports {
'indexers': string;
'modalBody': string;
'modalFooter': string;
+ 'notice': string;
'scroller': string;
}
export const cssExports: CssExports;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.js b/frontend/src/Indexer/Add/AddIndexerModalContent.js
deleted file mode 100644
index f4050560e..000000000
--- a/frontend/src/Indexer/Add/AddIndexerModalContent.js
+++ /dev/null
@@ -1,324 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Alert from 'Components/Alert';
-import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
-import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
-import TextInput from 'Components/Form/TextInput';
-import Button from 'Components/Link/Button';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-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 Scroller from 'Components/Scroller/Scroller';
-import Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import { kinds, scrollDirections } from 'Helpers/Props';
-import getErrorMessage from 'Utilities/Object/getErrorMessage';
-import translate from 'Utilities/String/translate';
-import SelectIndexerRow from './SelectIndexerRow';
-import styles from './AddIndexerModalContent.css';
-
-const columns = [
- {
- name: 'protocol',
- label: () => translate('Protocol'),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'sortName',
- label: () => translate('Name'),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'language',
- label: () => translate('Language'),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'description',
- label: () => translate('Description'),
- isSortable: false,
- isVisible: true
- },
- {
- name: 'privacy',
- label: () => translate('Privacy'),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'categories',
- label: () => translate('Categories'),
- isSortable: false,
- isVisible: true
- }
-];
-
-const protocols = [
- {
- key: 'torrent',
- value: 'torrent'
- },
- {
- key: 'usenet',
- value: 'nzb'
- }
-];
-
-const privacyLevels = [
- {
- key: 'private',
- get value() {
- return translate('Private');
- }
- },
- {
- key: 'semiPrivate',
- get value() {
- return translate('SemiPrivate');
- }
- },
- {
- key: 'public',
- get value() {
- return translate('Public');
- }
- }
-];
-
-class AddIndexerModalContent extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- filter: '',
- filterProtocols: [],
- filterLanguages: [],
- filterPrivacyLevels: [],
- filterCategories: []
- };
- }
-
- //
- // Listeners
-
- onFilterChange = ({ value }) => {
- this.setState({ filter: value });
- };
-
- //
- // Render
-
- render() {
- const {
- indexers,
- onIndexerSelect,
- sortKey,
- sortDirection,
- isFetching,
- isPopulated,
- error,
- onSortPress,
- onModalClose
- } = this.props;
-
- const languages = Array.from(new Set(indexers.map(({ language }) => language)))
- .sort((a, b) => a.localeCompare(b))
- .map((language) => ({ key: language, value: language }));
-
- const filteredIndexers = indexers.filter((indexer) => {
- const {
- filter,
- filterProtocols,
- filterLanguages,
- filterPrivacyLevels,
- filterCategories
- } = this.state;
-
- if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
- return false;
- }
-
- if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) {
- return false;
- }
-
- if (filterLanguages.length && !filterLanguages.includes(indexer.language)) {
- return false;
- }
-
- if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) {
- return false;
- }
-
- if (filterCategories.length) {
- const { categories = [] } = indexer.capabilities || {};
- const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
- const flatCategories = categories
- .filter((item) => item.id < 100000)
- .flatMap(flat);
-
- if (!filterCategories.every((item) => flatCategories.includes(item))) {
- return false;
- }
- }
-
- return true;
- });
-
- const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
-
- return (
-
-
- {translate('AddIndexer')}
-
-
-
-
-
-
-
-
- this.setState({ filterProtocols: value })}
- />
-
-
-
-
- this.setState({ filterLanguages: value })}
- />
-
-
-
-
- this.setState({ filterPrivacyLevels: value })}
- />
-
-
-
-
- this.setState({ filterCategories: value })}
- />
-
-
-
-
-
- {translate('ProwlarrSupportsAnyIndexer')}
-
-
-
-
- {
- isFetching ? : null
- }
- {
- error ? {errorMessage} : null
- }
- {
- isPopulated && !!indexers.length ?
-
-
- {
- filteredIndexers.map((indexer) => (
-
- ))
- }
-
-
:
- null
- }
- {
- isPopulated && !!indexers.length && !filteredIndexers.length ?
-
- {translate('NoIndexersFound')}
- :
- null
- }
-
-
-
-
-
- {
- isPopulated ?
- translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
- null
- }
-
-
-
-
-
-
-
- );
- }
-}
-
-AddIndexerModalContent.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- error: PropTypes.object,
- sortKey: PropTypes.string,
- sortDirection: PropTypes.string,
- onSortPress: PropTypes.func.isRequired,
- indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
- onIndexerSelect: PropTypes.func.isRequired,
- onModalClose: PropTypes.func.isRequired
-};
-
-export default AddIndexerModalContent;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx
new file mode 100644
index 000000000..be1413769
--- /dev/null
+++ b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx
@@ -0,0 +1,434 @@
+import { some } from 'lodash';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
+import IndexerAppState from 'App/State/IndexerAppState';
+import Alert from 'Components/Alert';
+import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
+import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
+import TextInput from 'Components/Form/TextInput';
+import Button from 'Components/Link/Button';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+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 Scroller from 'Components/Scroller/Scroller';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import { kinds, scrollDirections } from 'Helpers/Props';
+import Indexer, { IndexerCategory } from 'Indexer/Indexer';
+import {
+ fetchIndexerSchema,
+ selectIndexerSchema,
+ setIndexerSchemaSort,
+} from 'Store/Actions/indexerActions';
+import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
+import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
+import { SortCallback } from 'typings/callbacks';
+import sortByProp from 'Utilities/Array/sortByProp';
+import getErrorMessage from 'Utilities/Object/getErrorMessage';
+import translate from 'Utilities/String/translate';
+import SelectIndexerRow from './SelectIndexerRow';
+import styles from './AddIndexerModalContent.css';
+
+const COLUMNS = [
+ {
+ name: 'protocol',
+ label: () => translate('Protocol'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'sortName',
+ label: () => translate('Name'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'language',
+ label: () => translate('Language'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'description',
+ label: () => translate('Description'),
+ isSortable: false,
+ isVisible: true,
+ },
+ {
+ name: 'privacy',
+ label: () => translate('Privacy'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'categories',
+ label: () => translate('Categories'),
+ isSortable: false,
+ isVisible: true,
+ },
+];
+
+const PROTOCOLS = [
+ {
+ key: 'torrent',
+ value: 'torrent',
+ },
+ {
+ key: 'usenet',
+ value: 'nzb',
+ },
+];
+
+const PRIVACY_LEVELS = [
+ {
+ key: 'private',
+ get value() {
+ return translate('Private');
+ },
+ },
+ {
+ key: 'semiPrivate',
+ get value() {
+ return translate('SemiPrivate');
+ },
+ },
+ {
+ key: 'public',
+ get value() {
+ return translate('Public');
+ },
+ },
+];
+
+interface IndexerSchema extends Indexer {
+ isExistingIndexer: boolean;
+}
+
+function createAddIndexersSelector() {
+ return createSelector(
+ createClientSideCollectionSelector('indexers.schema'),
+ createAllIndexersSelector(),
+ (indexers: IndexerAppState, allIndexers) => {
+ const { isFetching, isPopulated, error, items, sortDirection, sortKey } =
+ indexers;
+
+ const indexerList: IndexerSchema[] = items.map((item) => {
+ const { definitionName } = item;
+ return {
+ ...item,
+ isExistingIndexer: some(allIndexers, { definitionName }),
+ };
+ });
+
+ return {
+ isFetching,
+ isPopulated,
+ error,
+ indexers: indexerList,
+ sortKey,
+ sortDirection,
+ };
+ }
+ );
+}
+
+interface AddIndexerModalContentProps {
+ onSelectIndexer(): void;
+ onModalClose(): void;
+}
+
+function AddIndexerModalContent(props: AddIndexerModalContentProps) {
+ const { onSelectIndexer, onModalClose } = props;
+
+ const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } =
+ useSelector(createAddIndexersSelector());
+ const dispatch = useDispatch();
+
+ const [filter, setFilter] = useState('');
+ const [filterProtocols, setFilterProtocols] = useState([]);
+ const [filterLanguages, setFilterLanguages] = useState([]);
+ const [filterPrivacyLevels, setFilterPrivacyLevels] = useState([]);
+ const [filterCategories, setFilterCategories] = useState([]);
+
+ useEffect(
+ () => {
+ dispatch(fetchIndexerSchema());
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+
+ const onFilterChange = useCallback(
+ ({ value }: { value: string }) => {
+ setFilter(value);
+ },
+ [setFilter]
+ );
+
+ const onFilterProtocolsChange = useCallback(
+ ({ value }: { value: string[] }) => {
+ setFilterProtocols(value);
+ },
+ [setFilterProtocols]
+ );
+
+ const onFilterLanguagesChange = useCallback(
+ ({ value }: { value: string[] }) => {
+ setFilterLanguages(value);
+ },
+ [setFilterLanguages]
+ );
+
+ const onFilterPrivacyLevelsChange = useCallback(
+ ({ value }: { value: string[] }) => {
+ setFilterPrivacyLevels(value);
+ },
+ [setFilterPrivacyLevels]
+ );
+
+ const onFilterCategoriesChange = useCallback(
+ ({ value }: { value: number[] }) => {
+ setFilterCategories(value);
+ },
+ [setFilterCategories]
+ );
+
+ const onIndexerSelect = useCallback(
+ ({
+ implementation,
+ implementationName,
+ name,
+ }: {
+ implementation: string;
+ implementationName: string;
+ name: string;
+ }) => {
+ dispatch(
+ selectIndexerSchema({
+ implementation,
+ implementationName,
+ name,
+ })
+ );
+
+ onSelectIndexer();
+ },
+ [dispatch, onSelectIndexer]
+ );
+
+ const onSortPress = useCallback(
+ (sortKey, sortDirection) => {
+ dispatch(setIndexerSchemaSort({ sortKey, sortDirection }));
+ },
+ [dispatch]
+ );
+
+ const languages = useMemo(
+ () =>
+ Array.from(new Set(indexers.map(({ language }) => language)))
+ .map((language) => ({ key: language, value: language }))
+ .sort(sortByProp('value')),
+ [indexers]
+ );
+
+ const filteredIndexers = useMemo(() => {
+ const flat = ({
+ id,
+ subCategories = [],
+ }: {
+ id: number;
+ subCategories: IndexerCategory[];
+ }): number[] => [id, ...subCategories.flatMap(flat)];
+
+ return indexers.filter((indexer) => {
+ if (
+ filter.length &&
+ !indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) &&
+ !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())
+ ) {
+ return false;
+ }
+
+ if (
+ filterProtocols.length &&
+ !filterProtocols.includes(indexer.protocol)
+ ) {
+ return false;
+ }
+
+ if (
+ filterLanguages.length &&
+ !filterLanguages.includes(indexer.language)
+ ) {
+ return false;
+ }
+
+ if (
+ filterPrivacyLevels.length &&
+ !filterPrivacyLevels.includes(indexer.privacy)
+ ) {
+ return false;
+ }
+
+ if (filterCategories.length) {
+ const { categories = [] } = indexer.capabilities || {};
+
+ const flatCategories = categories
+ .filter((item) => item.id < 100000)
+ .flatMap(flat);
+
+ if (
+ !filterCategories.every((categoryId) =>
+ flatCategories.includes(categoryId)
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }, [
+ indexers,
+ filter,
+ filterProtocols,
+ filterLanguages,
+ filterPrivacyLevels,
+ filterCategories,
+ ]);
+
+ const errorMessage = getErrorMessage(
+ error,
+ translate('UnableToLoadIndexers')
+ );
+
+ return (
+
+ {translate('AddIndexer')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {translate('ProwlarrSupportsAnyIndexer')}
+
+
+
+ {isFetching ? : null}
+
+ {error ? (
+
+ {errorMessage}
+
+ ) : null}
+
+ {isPopulated && !!indexers.length ? (
+
+
+ {filteredIndexers.map((indexer) => (
+
+ ))}
+
+
+ ) : null}
+
+ {isPopulated && !!indexers.length && !filteredIndexers.length ? (
+
+ {translate('NoIndexersFound')}
+
+ ) : null}
+
+
+
+
+
+ {isPopulated
+ ? translate('CountIndexersAvailable', {
+ count: filteredIndexers.length,
+ })
+ : null}
+
+
+
+
+
+
+
+ );
+}
+
+export default AddIndexerModalContent;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js
deleted file mode 100644
index a422e0a03..000000000
--- a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { some } from 'lodash';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
-import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
-import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
-import AddIndexerModalContent from './AddIndexerModalContent';
-
-function createMapStateToProps() {
- return createSelector(
- createClientSideCollectionSelector('indexers.schema'),
- createAllIndexersSelector(),
- (indexers, allIndexers) => {
- const {
- isFetching,
- isPopulated,
- error,
- items,
- sortDirection,
- sortKey
- } = indexers;
-
- const indexerList = items.map((item) => {
- const { definitionName } = item;
- return {
- ...item,
- isExistingIndexer: some(allIndexers, { definitionName })
- };
- });
-
- return {
- isFetching,
- isPopulated,
- error,
- indexers: indexerList,
- sortKey,
- sortDirection
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- fetchIndexerSchema,
- selectIndexerSchema,
- setIndexerSchemaSort
-};
-
-class AddIndexerModalContentConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- this.props.fetchIndexerSchema();
- }
-
- //
- // Listeners
-
- onIndexerSelect = ({ implementation, implementationName, name }) => {
- this.props.selectIndexerSchema({ implementation, implementationName, name });
- this.props.onSelectIndexer();
- };
-
- onSortPress = (sortKey, sortDirection) => {
- this.props.setIndexerSchemaSort({ sortKey, sortDirection });
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-AddIndexerModalContentConnector.propTypes = {
- fetchIndexerSchema: PropTypes.func.isRequired,
- selectIndexerSchema: PropTypes.func.isRequired,
- setIndexerSchemaSort: PropTypes.func.isRequired,
- onModalClose: PropTypes.func.isRequired,
- onSelectIndexer: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.tsx b/frontend/src/Indexer/Add/SelectIndexerRow.tsx
index ab6850573..157050e41 100644
--- a/frontend/src/Indexer/Add/SelectIndexerRow.tsx
+++ b/frontend/src/Indexer/Add/SelectIndexerRow.tsx
@@ -2,18 +2,19 @@ import React, { useCallback } from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
+import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons } from 'Helpers/Props';
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
+import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
-import { IndexerCapabilities } from 'Indexer/Indexer';
-import firstCharToUpper from 'Utilities/String/firstCharToUpper';
+import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerRow.css';
interface SelectIndexerRowProps {
name: string;
- protocol: string;
- privacy: string;
+ protocol: DownloadProtocol;
+ privacy: IndexerPrivacy;
language: string;
description: string;
capabilities: IndexerCapabilities;
@@ -63,7 +64,9 @@ function SelectIndexerRow(props: SelectIndexerRowProps) {
{description}
- {translate(firstCharToUpper(privacy))}
+
+
+
diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContent.js b/frontend/src/Indexer/Edit/EditIndexerModalContent.js
index b09b9e656..7dabc50d9 100644
--- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js
+++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js
@@ -97,7 +97,7 @@ function EditIndexerModalContent(props) {
@@ -144,6 +144,7 @@ function EditIndexerModalContent(props) {
}) :
null
}
+
{
);
const [isSelectMode, setIsSelectMode] = useState(false);
+ useEffect(() => {
+ dispatch(fetchIndexers());
+ dispatch(fetchIndexerStatus());
+ }, [dispatch]);
+
const onAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true);
}, [setIsAddIndexerModalOpen]);
diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx
index b0bba5b94..9d42aa389 100644
--- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx
+++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx
@@ -19,6 +19,7 @@ interface SavePayload {
seedRatio?: number;
seedTime?: number;
packSeedTime?: number;
+ preferMagnetUrl?: boolean;
}
interface EditIndexerModalContentProps {
@@ -35,7 +36,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
- disabled: true,
+ isDisabled: true,
},
{
key: 'true',
@@ -65,6 +66,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
const [packSeedTime, setPackSeedTime] = useState(
null
);
+ const [preferMagnetUrl, setPreferMagnetUrl] = useState<
+ null | string | boolean
+ >(null);
const save = useCallback(() => {
let hasChanges = false;
@@ -105,6 +109,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
payload.packSeedTime = packSeedTime as number;
}
+ if (preferMagnetUrl !== null) {
+ hasChanges = true;
+ payload.preferMagnetUrl = preferMagnetUrl === 'true';
+ }
+
if (hasChanges) {
onSavePress(payload);
}
@@ -118,6 +127,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
seedRatio,
seedTime,
packSeedTime,
+ preferMagnetUrl,
onSavePress,
onModalClose,
]);
@@ -146,6 +156,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
case 'packSeedTime':
setPackSeedTime(value);
break;
+ case 'preferMagnetUrl':
+ setPreferMagnetUrl(value);
+ break;
default:
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
}
@@ -224,6 +237,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
name="seedRatio"
value={seedRatio}
helpText={translate('SeedRatioHelpText')}
+ isFloat={true}
onChange={onInputChange}
/>
@@ -253,6 +267,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
onChange={onInputChange}
/>
+
+
+ {translate('PreferMagnetUrl')}
+
+
+
diff --git a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx
index c832806ed..8e30532cc 100644
--- a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx
+++ b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx
@@ -2,6 +2,7 @@ import { uniqBy } from 'lodash';
import React from 'react';
import Label from 'Components/Label';
import { IndexerCapabilities } from 'Indexer/Indexer';
+import translate from 'Utilities/String/translate';
interface CapabilitiesLabelProps {
capabilities: IndexerCapabilities;
@@ -38,7 +39,7 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) {
);
})}
- {filteredList.length === 0 ? : null}
+ {filteredList.length === 0 ? : null}
);
}
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css
index a0a0daee4..a20efded3 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css
@@ -11,6 +11,12 @@
flex: 0 0 60px;
}
+.id {
+ composes: cell;
+
+ flex: 0 0 60px;
+}
+
.sortName {
composes: cell;
@@ -23,7 +29,8 @@
.minimumSeeders,
.seedRatio,
.seedTime,
-.packSeedTime {
+.packSeedTime,
+.preferMagnetUrl {
composes: cell;
flex: 0 0 90px;
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts
index c5d22cf6d..42821bd74 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts
@@ -8,8 +8,10 @@ interface CssExports {
'cell': string;
'checkInput': string;
'externalLink': string;
+ 'id': string;
'minimumSeeders': string;
'packSeedTime': string;
+ 'preferMagnetUrl': string;
'priority': string;
'privacy': string;
'protocol': string;
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx
index d406750ac..e4c3cd32e 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
-import Label from 'Components/Label';
+import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
@@ -15,10 +15,10 @@ import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItem
import Indexer from 'Indexer/Indexer';
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
import { SelectStateInputProps } from 'typings/props';
-import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
import IndexerStatusCell from './IndexerStatusCell';
+import PrivacyLabel from './PrivacyLabel';
import ProtocolLabel from './ProtocolLabel';
import styles from './IndexerIndexRow.css';
@@ -34,7 +34,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
const { indexerId, columns, isSelectMode, onCloneIndexerPress } = props;
const { indexer, appProfile, status, longDateFormat, timeFormat } =
- useSelector(createIndexerIndexItemSelector(props.indexerId));
+ useSelector(createIndexerIndexItemSelector(indexerId));
const {
id,
@@ -75,6 +75,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
?.value ?? undefined;
+ const preferMagnetUrl =
+ fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')
+ ?.value ?? undefined;
+
const rssUrl = `${window.location.origin}${
window.Prowlarr.urlBase
}/${id}/api?apikey=${encodeURIComponent(
@@ -103,6 +107,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
setIsDeleteIndexerModalOpen(false);
}, [setIsDeleteIndexerModalOpen]);
+ const checkInputCallback = useCallback(() => {
+ // Mock handler to satisfy `onChange` being required for `CheckInput`.
+ }, []);
+
const onSelectedChange = useCallback(
({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({
@@ -148,12 +156,24 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
);
}
+ if (name === 'id') {
+ return (
+
+
+
+ );
+ }
+
if (name === 'sortName') {
return (
@@ -163,7 +183,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
if (name === 'privacy') {
return (
-
+
);
}
@@ -266,6 +286,21 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
);
}
+ if (name === 'preferMagnetUrl') {
+ return (
+
+ {preferMagnetUrl === undefined ? null : (
+
+ )}
+
+ );
+ }
+
if (name === 'actions') {
return (
columns
);
-const Row: React.FC> = ({
- index,
- style,
- data,
-}) => {
+function Row({ index, style, data }: ListChildComponentProps) {
const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data;
if (index >= items.length) {
@@ -77,7 +73,7 @@ const Row: React.FC> = ({
/>
);
-};
+}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css
index 90ad3b0e9..839cd49ff 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css
@@ -4,6 +4,12 @@
flex: 0 0 60px;
}
+.id {
+ composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
+
+ flex: 0 0 60px;
+}
+
.sortName {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
@@ -16,7 +22,8 @@
.minimumSeeders,
.seedRatio,
.seedTime,
-.packSeedTime {
+.packSeedTime,
+.preferMagnetUrl {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 90px;
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts
index 94d39a9bb..020d61f27 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts
@@ -5,8 +5,10 @@ interface CssExports {
'added': string;
'appProfileId': string;
'capabilities': string;
+ 'id': string;
'minimumSeeders': string;
'packSeedTime': string;
+ 'preferMagnetUrl': string;
'priority': string;
'privacy': string;
'protocol': string;
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx
index 6155929df..3aa087790 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx
@@ -1,4 +1,4 @@
-import React, { Fragment, useCallback } from 'react';
+import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -32,19 +32,17 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
);
return (
-