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",