diff --git a/frontend/src/Components/Table/VirtualTableRowButton.css b/frontend/src/Components/Table/VirtualTableRowButton.css
new file mode 100644
index 000000000..886765f2a
--- /dev/null
+++ b/frontend/src/Components/Table/VirtualTableRowButton.css
@@ -0,0 +1,4 @@
+.row {
+ composes: link from '~Components/Link/Link.css';
+ composes: row from '~./VirtualTableRow.css';
+}
diff --git a/frontend/src/Components/Table/VirtualTableRowButton.css.d.ts b/frontend/src/Components/Table/VirtualTableRowButton.css.d.ts
new file mode 100644
index 000000000..d4b245cd1
--- /dev/null
+++ b/frontend/src/Components/Table/VirtualTableRowButton.css.d.ts
@@ -0,0 +1,7 @@
+// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ 'row': string;
+}
+export const cssExports: CssExports;
+export default cssExports;
diff --git a/frontend/src/Components/Table/VirtualTableRowButton.js b/frontend/src/Components/Table/VirtualTableRowButton.js
new file mode 100644
index 000000000..ba63c1648
--- /dev/null
+++ b/frontend/src/Components/Table/VirtualTableRowButton.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import Link from 'Components/Link/Link';
+import VirtualTableRow from './VirtualTableRow';
+import styles from './VirtualTableRowButton.css';
+
+function VirtualTableRowButton(props) {
+ return (
+
+ );
+}
+
+export default VirtualTableRowButton;
diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx
index 038cdd2cc..15e377209 100644
--- a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx
+++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.tsx
@@ -1,5 +1,13 @@
-import React, { useCallback, useMemo, useState } from 'react';
+import { throttle } from 'lodash';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { useSelector } from 'react-redux';
+import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -7,24 +15,136 @@ 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 Column from 'Components/Table/Column';
+import VirtualTableRowButton from 'Components/Table/VirtualTableRowButton';
import { scrollDirections } from 'Helpers/Props';
import Series from 'Series/Series';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
+import dimensions from 'Styles/Variables/dimensions';
import translate from 'Utilities/String/translate';
+import SelectSeriesModalTableHeader from './SelectSeriesModalTableHeader';
import SelectSeriesRow from './SelectSeriesRow';
import styles from './SelectSeriesModalContent.css';
+const columns = [
+ {
+ name: 'title',
+ label: () => translate('Title'),
+ isVisible: true,
+ },
+ {
+ name: 'year',
+ label: () => translate('Year'),
+ isVisible: true,
+ },
+ {
+ name: 'tvdbId',
+ label: () => translate('TvdbId'),
+ isVisible: true,
+ },
+ {
+ name: 'imdbId',
+ label: () => translate('ImdbId'),
+ isVisible: true,
+ },
+];
+
+const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
+
interface SelectSeriesModalContentProps {
modalTitle: string;
onSeriesSelect(series: Series): void;
onModalClose(): void;
}
+interface RowItemData {
+ items: Series[];
+ columns: Column[];
+ onSeriesSelect(seriesId: number): void;
+}
+
+const Row: React.FC> = ({
+ index,
+ style,
+ data,
+}) => {
+ const { items, columns, onSeriesSelect } = data;
+
+ if (index >= items.length) {
+ return null;
+ }
+
+ const series = items[index];
+
+ return (
+ onSeriesSelect(series.id)}
+ >
+
+
+ );
+};
+
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
const { modalTitle, onSeriesSelect, onModalClose } = props;
+ const listRef = useRef>(null);
+ const scrollerRef = useRef(null);
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const [filter, setFilter] = useState('');
+ const [size, setSize] = useState({ width: 0, height: 0 });
+ const windowHeight = window.innerHeight;
+
+ useEffect(() => {
+ const current = scrollerRef?.current as HTMLElement;
+
+ if (current) {
+ const width = current.clientWidth;
+ const height = current.clientHeight;
+ const padding = bodyPadding - 5;
+
+ setSize({
+ width: width - padding * 2,
+ height: height + padding,
+ });
+ }
+ }, [windowHeight, scrollerRef]);
+
+ useEffect(() => {
+ const currentScrollerRef = scrollerRef.current as HTMLElement;
+ const currentScrollListener = currentScrollerRef;
+
+ const handleScroll = throttle(() => {
+ const { offsetTop = 0 } = currentScrollerRef;
+ const scrollTop = currentScrollerRef.scrollTop - offsetTop;
+
+ listRef.current?.scrollTo(scrollTop);
+ }, 10);
+
+ currentScrollListener.addEventListener('scroll', handleScroll);
+
+ return () => {
+ handleScroll.cancel();
+
+ if (currentScrollListener) {
+ currentScrollListener.removeEventListener('scroll', handleScroll);
+ }
+ };
+ }, [listRef, scrollerRef]);
const onFilterChange = useCallback(
({ value }: { value: string }) => {
@@ -47,8 +167,11 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
a.sortTitle.localeCompare(b.sortTitle)
);
- return sorted.filter((item) =>
- item.title.toLowerCase().includes(filter.toLowerCase())
+ return sorted.filter(
+ (item) =>
+ item.title.toLowerCase().includes(filter.toLowerCase()) ||
+ item.tvdbId.toString().includes(filter) ||
+ item.imdbId?.includes(filter)
);
}, [allSeries, filter]);
@@ -69,17 +192,31 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
onChange={onFilterChange}
/>
-
- {items.map((item) => {
- return (
-
- );
- })}
+
+
+
+ ref={listRef}
+ style={{
+ width: '100%',
+ height: '100%',
+ overflow: 'none',
+ }}
+ width={size.width}
+ height={size.height}
+ itemCount={items.length}
+ itemSize={38}
+ itemData={{
+ items,
+ columns,
+ onSeriesSelect: onSeriesSelectWrapper,
+ }}
+ >
+ {Row}
+
diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.css b/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.css
new file mode 100644
index 000000000..990b6e8f8
--- /dev/null
+++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.css
@@ -0,0 +1,18 @@
+.title {
+ composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
+
+ flex: 4 0 140px;
+}
+
+.year {
+ composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
+
+ flex: 0 0 70px;
+}
+
+.imdbId,
+.tvdbId {
+ composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
+
+ flex: 0 0 110px;
+}
diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.css.d.ts b/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.css.d.ts
new file mode 100644
index 000000000..bca14a6d5
--- /dev/null
+++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.css.d.ts
@@ -0,0 +1,10 @@
+// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ 'imdbId': string;
+ 'title': string;
+ 'tvdbId': string;
+ 'year': string;
+}
+export const cssExports: CssExports;
+export default cssExports;
diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.tsx b/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.tsx
new file mode 100644
index 000000000..eec60c8af
--- /dev/null
+++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalTableHeader.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import Column from 'Components/Table/Column';
+import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
+import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
+import styles from './SelectSeriesModalTableHeader.css';
+
+interface SelectSeriesModalTableHeaderProps {
+ columns: Column[];
+}
+
+function SelectSeriesModalTableHeader(
+ props: SelectSeriesModalTableHeaderProps
+) {
+ const { columns } = props;
+
+ return (
+
+ {columns.map((column) => {
+ const { name, label, isVisible } = column;
+
+ if (!isVisible) {
+ return null;
+ }
+
+ return (
+
+ {typeof label === 'function' ? label() : label}
+
+ );
+ })}
+
+ );
+}
+
+export default SelectSeriesModalTableHeader;
diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesRow.css b/frontend/src/InteractiveImport/Series/SelectSeriesRow.css
index 9efd3d79b..733e2b70c 100644
--- a/frontend/src/InteractiveImport/Series/SelectSeriesRow.css
+++ b/frontend/src/InteractiveImport/Series/SelectSeriesRow.css
@@ -1,4 +1,25 @@
-.series {
- padding: 8px;
- border-bottom: 1px solid var(--borderColor);
+.cell {
+ composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
+
+ display: flex;
+ align-items: center;
+}
+
+.title {
+ composes: cell;
+
+ flex: 4 0 140px;
+}
+
+.year {
+ composes: cell;
+
+ flex: 0 0 70px;
+}
+
+.tvdbId,
+.imdbId {
+ composes: cell;
+
+ flex: 0 0 110px;
}
diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesRow.css.d.ts b/frontend/src/InteractiveImport/Series/SelectSeriesRow.css.d.ts
index f86badd52..bba0583e6 100644
--- a/frontend/src/InteractiveImport/Series/SelectSeriesRow.css.d.ts
+++ b/frontend/src/InteractiveImport/Series/SelectSeriesRow.css.d.ts
@@ -1,7 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
- 'series': string;
+ 'cell': string;
+ 'imdbId': string;
+ 'title': string;
+ 'tvdbId': string;
+ 'year': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesRow.js b/frontend/src/InteractiveImport/Series/SelectSeriesRow.js
index 15c092299..ce9d01f1b 100644
--- a/frontend/src/InteractiveImport/Series/SelectSeriesRow.js
+++ b/frontend/src/InteractiveImport/Series/SelectSeriesRow.js
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import Link from 'Components/Link/Link';
+import Label from 'Components/Label';
+import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import styles from './SelectSeriesRow.css';
class SelectSeriesRow extends Component {
@@ -17,13 +18,27 @@ class SelectSeriesRow extends Component {
render() {
return (
-
- {this.props.title}
-
+ <>
+
+ {this.props.title}
+
+
+
+ {this.props.year}
+
+
+
+
+
+
+
+ {
+ this.props.imdbId ?
+ :
+ null
+ }
+
+ >
);
}
}
@@ -31,6 +46,9 @@ class SelectSeriesRow extends Component {
SelectSeriesRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
+ tvdbId: PropTypes.number.isRequired,
+ imdbId: PropTypes.string,
+ year: PropTypes.number.isRequired,
onSeriesSelect: PropTypes.func.isRequired
};
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 500eb150f..4bbb83591 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -538,6 +538,7 @@
"Ignored": "Ignored",
"IgnoredAddresses": "Ignored Addresses",
"Images": "Images",
+ "ImdbId": "IMDb ID",
"Implementation": "Implementation",
"Import": "Import",
"ImportCountSeries": "Import {selectedCount} Series",