Refactor Indexer index to use react-window

(cherry picked from commit d022679b7dcbce3cec98e6a1fd0879e3c0d92523)
This commit is contained in:
Mark McDowall
2023-01-05 18:20:49 -08:00
committed by Qstick
parent c2599ef2e7
commit c0383ad5f5
314 changed files with 4928 additions and 3017 deletions
+12
View File
@@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'contentBody': string;
'contentBodyContainer': string;
'errorMessage': string;
'pageContentBodyWrapper': string;
'postersInnerContentBody': string;
'tableInnerContentBody': string;
}
export const cssExports: CssExports;
export default cssExports;
-516
View File
@@ -1,516 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import IndexerEditorFooter from 'Indexer/Editor/IndexerEditorFooter.js';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import IndexerIndexFooterConnector from './IndexerIndexFooterConnector';
import IndexerIndexFilterMenu from './Menus/IndexerIndexFilterMenu';
import IndexerIndexSortMenu from './Menus/IndexerIndexSortMenu';
import IndexerIndexTableConnector from './Table/IndexerIndexTableConnector';
import IndexerIndexTableOptionsConnector from './Table/IndexerIndexTableOptionsConnector';
import styles from './IndexerIndex.css';
function getViewComponent() {
return IndexerIndexTableConnector;
}
class IndexerIndex extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
scroller: null,
jumpBarItems: { order: [] },
jumpToCharacter: null,
isMovieEditorActive: false,
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false,
searchType: null,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
componentDidMount() {
this.setJumpBarItems();
this.setSelectedState();
window.addEventListener('keyup', this.onKeyUp);
}
componentDidUpdate(prevProps) {
const {
items,
sortKey,
sortDirection,
isDeleting,
deleteError
} = this.props;
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
this.setSelectedState();
}
if (this.state.jumpToCharacter != null) {
this.setState({ jumpToCharacter: null });
}
const hasFinishedDeleting = prevProps.isDeleting &&
!isDeleting &&
!deleteError;
if (hasFinishedDeleting) {
this.onSelectAllChange({ value: false });
}
}
//
// Control
setScrollerRef = (ref) => {
this.setState({ scroller: ref });
};
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
};
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((movie) => {
const isItemSelected = selectedState[movie.id];
if (isItemSelected) {
newSelectedState[movie.id] = isItemSelected;
} else {
newSelectedState[movie.id] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
setJumpBarItems() {
const {
items,
sortKey,
sortDirection
} = this.props;
// Reset if not sorting by sortTitle
if (sortKey !== 'sortTitle') {
this.setState({ jumpBarItems: { order: [] } });
return;
}
const characters = _.reduce(items, (acc, item) => {
let char = item.sortTitle.charAt(0);
if (!isNaN(char)) {
char = '#';
}
if (char in acc) {
acc[char] = acc[char] + 1;
} else {
acc[char] = 1;
}
return acc;
}, {});
const order = Object.keys(characters).sort();
// Reverse if sorting descending
if (sortDirection === sortDirections.DESCENDING) {
order.reverse();
}
const jumpBarItems = {
characters,
order
};
this.setState({ jumpBarItems });
}
//
// Listeners
onAddIndexerPress = () => {
this.setState({ isAddIndexerModalOpen: true });
};
onAddIndexerModalClose = () => {
this.setState({ isAddIndexerModalOpen: false });
};
onAddIndexerSelectIndexer = () => {
this.setState({ isEditIndexerModalOpen: true });
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onMovieEditorTogglePress = () => {
if (this.state.isMovieEditorActive) {
this.setState({ isMovieEditorActive: false });
} else {
const newState = selectAll(this.state.selectedState, false);
newState.isMovieEditorActive = true;
this.setState(newState);
}
};
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
};
onKeyUp = (event) => {
const jumpBarItems = this.state.jumpBarItems.order;
if (event.composedPath && event.composedPath().length === 4) {
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
this.setState({ jumpToCharacter: jumpBarItems[0] });
}
if (event.keyCode === keyCodes.END && event.ctrlKey) {
this.setState({ jumpToCharacter: jumpBarItems[jumpBarItems.length - 1] });
}
}
};
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onSaveSelected = (changes) => {
this.props.onSaveSelected({
indexerIds: this.getSelectedIds(),
...changes
});
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
isSaving,
saveError,
isDeleting,
isTestingAll,
isSyncingIndexers,
deleteError,
onScroll,
onSortSelect,
onFilterSelect,
onTestAllPress,
...otherProps
} = this.props;
const {
scroller,
jumpBarItems,
jumpToCharacter,
isAddIndexerModalOpen,
isEditIndexerModalOpen,
isMovieEditorActive,
selectedState,
allSelected,
allUnselected
} = this.state;
const selectedIndexerIds = this.getSelectedIds();
const ViewComponent = getViewComponent();
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('AddIndexer')}
iconName={icons.ADD}
spinningName={icons.ADD}
onPress={this.onAddIndexerPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SyncAppIndexers')}
iconName={icons.REFRESH}
isSpinning={isSyncingIndexers}
onPress={this.props.onAppIndexerSyncPress}
/>
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
isDisabled={hasNoIndexer}
onPress={this.props.onTestAllPress}
/>
<PageToolbarSeparator />
{
isMovieEditorActive ?
<PageToolbarButton
label={translate('Indexers')}
iconName={icons.MOVIE_CONTINUING}
isDisabled={hasNoIndexer}
onPress={this.onMovieEditorTogglePress}
/> :
<PageToolbarButton
label={translate('MassEditor')}
iconName={icons.EDIT}
isDisabled={hasNoIndexer}
onPress={this.onMovieEditorTogglePress}
/>
}
{
isMovieEditorActive ?
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoIndexer}
onPress={this.onSelectAllPress}
/> :
null
}
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
optionsComponent={IndexerIndexTableOptionsConnector}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper> :
null
<PageToolbarSeparator />
<IndexerIndexSortMenu
sortKey={sortKey}
sortDirection={sortDirection}
isDisabled={hasNoIndexer}
onSortSelect={onSortSelect}
/>
<IndexerIndexFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
isDisabled={hasNoIndexer}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBody
registerScroller={this.setScrollerRef}
className={styles.contentBody}
innerClassName={styles.tableInnerContentBody}
onScroll={onScroll}
>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.errorMessage}>
{getErrorMessage(error, 'Failed to load indexers from API')}
</div>
}
{
isLoaded &&
<div className={styles.contentBodyContainer}>
<ViewComponent
scroller={scroller}
items={items}
filters={filters}
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
isMovieEditorActive={isMovieEditorActive}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps}
/>
{
!isMovieEditorActive &&
<IndexerIndexFooterConnector />
}
</div>
}
{
!error && isPopulated && !items.length &&
<NoIndexer
totalItems={totalItems}
onAddIndexerPress={this.onAddIndexerPress}
/>
}
</PageContentBody>
{
isLoaded && !!jumpBarItems.order.length &&
<PageJumpBar
items={jumpBarItems}
onItemPress={this.onJumpBarItemPress}
/>
}
</div>
{
isLoaded && isMovieEditorActive &&
<IndexerEditorFooter
indexerIds={selectedIndexerIds}
selectedCount={selectedIndexerIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
onSaveSelected={this.onSaveSelected}
onOrganizeMoviePress={this.onOrganizeMoviePress}
/>
}
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={this.onAddIndexerModalClose}
onSelectIndexer={this.onAddIndexerSelectIndexer}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
/>
</PageContent>
);
}
}
IndexerIndex.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isSmallScreen: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
isTestingAll: PropTypes.bool.isRequired,
isSyncingIndexers: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onTestAllPress: PropTypes.func.isRequired,
onAppIndexerSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};
export default IndexerIndex;
+326
View File
@@ -0,0 +1,326 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import { APP_INDEXER_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import withScrollPosition from 'Components/withScrollPosition';
import { align, icons } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllIndexers } from 'Store/Actions/indexerActions';
import {
setIndexerFilter,
setIndexerSort,
setIndexerTableOption,
} from 'Store/Actions/indexerIndexActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
import translate from 'Utilities/String/translate';
import IndexerIndexFooter from './IndexerIndexFooter';
import IndexerIndexFilterMenu from './Menus/IndexerIndexFilterMenu';
import IndexerIndexSortMenu from './Menus/IndexerIndexSortMenu';
import IndexerIndexSelectAllButton from './Select/IndexerIndexSelectAllButton';
import IndexerIndexSelectAllMenuItem from './Select/IndexerIndexSelectAllMenuItem';
import IndexerIndexSelectFooter from './Select/IndexerIndexSelectFooter';
import IndexerIndexSelectModeButton from './Select/IndexerIndexSelectModeButton';
import IndexerIndexSelectModeMenuItem from './Select/IndexerIndexSelectModeMenuItem';
import IndexerIndexTable from './Table/IndexerIndexTable';
import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions';
import styles from './IndexerIndex.css';
function getViewComponent() {
return IndexerIndexTable;
}
interface IndexerIndexProps {
initialScrollTop?: number;
}
const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
const {
isFetching,
isPopulated,
isTestingAll,
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
view,
} = useSelector(
createIndexerClientSideCollectionItemsSelector('indexerIndex')
);
const isSyncingIndexers = useSelector(
createCommandExecutingSelector(APP_INDEXER_SYNC)
);
const { isSmallScreen } = useSelector(createDimensionsSelector());
const dispatch = useDispatch();
const scrollerRef = useRef<HTMLDivElement>();
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [isSelectMode, setIsSelectMode] = useState(false);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
})
);
}, [dispatch]);
const onAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true);
}, [setIsAddIndexerModalOpen]);
const onAddIndexerModalClose = useCallback(() => {
setIsAddIndexerModalOpen(false);
}, [setIsAddIndexerModalOpen]);
const onEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, [setIsEditIndexerModalOpen]);
const onEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
const onTestAllPress = useCallback(() => {
dispatch(testAllIndexers());
}, [dispatch]);
const onSelectModePress = useCallback(() => {
setIsSelectMode(!isSelectMode);
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback(
(payload) => {
dispatch(setIndexerTableOption(payload));
},
[dispatch]
);
const onSortSelect = useCallback(
(value) => {
dispatch(setIndexerSort({ sortKey: value }));
},
[dispatch]
);
const onFilterSelect = useCallback(
(value) => {
dispatch(setIndexerFilter({ selectedFilterKey: value }));
},
[dispatch]
);
const onJumpBarItemPress = useCallback(
(character) => {
setJumpToCharacter(character);
},
[setJumpToCharacter]
);
const onScroll = useCallback(
({ scrollTop }) => {
setJumpToCharacter(null);
scrollPositions.seriesIndex = scrollTop;
},
[setJumpToCharacter]
);
const jumpBarItems = useMemo(() => {
// Reset if not sorting by sortTitle
if (sortKey !== 'sortTitle') {
return {
order: [],
};
}
const characters = items.reduce((acc, item) => {
let char = item.sortTitle.charAt(0);
if (!isNaN(char)) {
char = '#';
}
if (char in acc) {
acc[char] = acc[char] + 1;
} else {
acc[char] = 1;
}
return acc;
}, {});
const order = Object.keys(characters).sort();
// Reverse if sorting descending
if (sortDirection === SortDirection.Descending) {
order.reverse();
}
return {
characters,
order,
};
}, [items, sortKey, sortDirection]);
const ViewComponent = useMemo(() => getViewComponent(), []);
const isLoaded = !!(!error && isPopulated && items.length);
const hasNoIndexer = !totalItems;
return (
<SelectProvider items={items}>
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('AddIndexer')}
iconName={icons.ADD}
spinningName={icons.ADD}
onPress={onAddIndexerPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SyncAppIndexers')}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isSyncingIndexers}
isDisabled={hasNoIndexer}
onPress={onAppIndexerSyncPress}
/>
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
isDisabled={hasNoIndexer}
onPress={onTestAllPress}
/>
<PageToolbarSeparator />
<IndexerIndexSelectModeButton
label={isSelectMode ? 'Stop Selecting' : 'Select Indexer'}
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
isSelectMode={isSelectMode}
overflowComponent={IndexerIndexSelectModeMenuItem}
onPress={onSelectModePress}
/>
<IndexerIndexSelectAllButton
label="SelectAll"
isSelectMode={isSelectMode}
overflowComponent={IndexerIndexSelectAllMenuItem}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
<TableOptionsModalWrapper
columns={columns}
optionsComponent={IndexerIndexTableOptions}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton label="Options" iconName={icons.TABLE} />
</TableOptionsModalWrapper>
<PageToolbarSeparator />
<IndexerIndexSortMenu
sortKey={sortKey}
sortDirection={sortDirection}
isDisabled={hasNoIndexer}
onSortSelect={onSortSelect}
/>
<IndexerIndexFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
isDisabled={hasNoIndexer}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}
>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? <div>Unable to load indexers</div> : null}
{isLoaded ? (
<div className={styles.contentBodyContainer}>
<ViewComponent
scrollerRef={scrollerRef}
items={items}
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
isSelectMode={isSelectMode}
isSmallScreen={isSmallScreen}
/>
<IndexerIndexFooter />
</div>
) : null}
{!error && isPopulated && !items.length ? (
<NoIndexer totalItems={totalItems} />
) : null}
</PageContentBody>
{isLoaded && !!jumpBarItems.order.length ? (
<PageJumpBar
items={jumpBarItems}
onItemPress={onJumpBarItemPress}
/>
) : null}
</div>
{isSelectMode ? <IndexerIndexSelectFooter /> : null}
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={onAddIndexerModalClose}
onSelectIndexer={onEditIndexerPress}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={onEditIndexerModalClose}
/>
</PageContent>
</SelectProvider>
);
}, 'indexerIndex');
export default IndexerIndex;
@@ -1,101 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllIndexers } from 'Store/Actions/indexerActions';
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
import IndexerIndex from './IndexerIndex';
function createMapStateToProps() {
return createSelector(
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
createDimensionsSelector(),
(
indexers,
isSyncingIndexers,
dimensionsState
) => {
return {
...indexers,
isSyncingIndexers,
isSmallScreen: dimensionsState.isSmallScreen
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onTableOptionChange(payload) {
dispatch(setMovieTableOption(payload));
},
onSortSelect(sortKey) {
dispatch(setMovieSort({ sortKey }));
},
onFilterSelect(selectedFilterKey) {
dispatch(setMovieFilter({ selectedFilterKey }));
},
dispatchSaveIndexerEditor(payload) {
dispatch(saveIndexerEditor(payload));
},
onTestAllPress() {
dispatch(testAllIndexers());
},
onAppIndexerSyncPress() {
dispatch(executeCommand({
name: commandNames.APP_INDEXER_SYNC
}));
}
};
}
class IndexerIndexConnector extends Component {
//
// Listeners
onSaveSelected = (payload) => {
this.props.dispatchSaveIndexerEditor(payload);
};
onScroll = ({ scrollTop }) => {
scrollPositions.movieIndex = scrollTop;
};
//
// Render
render() {
return (
<IndexerIndex
{...this.props}
onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
/>
);
}
}
IndexerIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
dispatchSaveIndexerEditor: PropTypes.func.isRequired,
items: PropTypes.arrayOf(PropTypes.object)
};
export default withScrollPosition(
connect(createMapStateToProps, createMapDispatchToProps)(IndexerIndexConnector),
'indexerIndex'
);
@@ -0,0 +1,48 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setIndexerFilter } from 'Store/Actions/indexerIndexActions';
function createIndexerSelector() {
return createSelector(
(state) => state.indexers.items,
(indexers) => {
return indexers;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state) => state.indexerIndex.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
export default function IndexerIndexFilterModal(props) {
const sectionItems = useSelector(createIndexerSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'indexerIndex';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload) => {
dispatch(setIndexerFilter(payload));
},
[dispatch]
);
return (
<FilterModal
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}
@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setMovieFilter } from 'Store/Actions/indexerIndexActions';
function createMapStateToProps() {
return createSelector(
(state) => state.indexers.items,
(state) => state.indexerIndex.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'indexerIndex'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setMovieFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
+14
View File
@@ -0,0 +1,14 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'disabled': string;
'enabled': string;
'error': string;
'footer': string;
'legendItem': string;
'legendItemColor': string;
'redirected': string;
'statistics': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,103 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
import styles from './IndexerIndexFooter.css';
class IndexerIndexFooter extends PureComponent {
render() {
const {
indexers,
colorImpairedMode
} = this.props;
const count = indexers.length;
let enabled = 0;
let torrent = 0;
indexers.forEach((s) => {
if (s.enable) {
enabled += 1;
}
if (s.protocol === 'torrent') {
torrent++;
}
});
return (
<div className={styles.footer}>
<div>
<div className={styles.legendItem}>
<div className={styles.enabled} />
<div>
{translate('Enabled')}
</div>
</div>
<div className={styles.legendItem}>
<div className={styles.redirected} />
<div>
{translate('EnabledRedirected')}
</div>
</div>
<div className={styles.legendItem}>
<div className={styles.disabled} />
<div>
{translate('Disabled')}
</div>
</div>
<div className={styles.legendItem}>
<div className={classNames(
styles.error,
colorImpairedMode && 'colorImpaired'
)}
/>
<div>
{translate('Error')}
</div>
</div>
</div>
<div className={styles.statistics}>
<DescriptionList>
<DescriptionListItem
title={translate('Indexers')}
data={count}
/>
<DescriptionListItem
title={translate('Enabled')}
data={enabled}
/>
</DescriptionList>
<DescriptionList>
<DescriptionListItem
title={translate('Torrent')}
data={torrent}
/>
<DescriptionListItem
title={translate('Usenet')}
data={count - torrent}
/>
</DescriptionList>
</div>
</div>
);
}
}
IndexerIndexFooter.propTypes = {
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
export default IndexerIndexFooter;
@@ -0,0 +1,115 @@
import classNames from 'classnames';
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
import translate from 'Utilities/String/translate';
import styles from './IndexerIndexFooter.css';
function createUnoptimizedSelector() {
return createSelector(
createClientSideCollectionSelector('indexers', 'indexerIndex'),
(indexers) => {
return indexers.items.map((s) => {
const { protocol, privacy, enable } = s;
return {
protocol,
privacy,
enable,
};
});
}
);
}
function createIndexersSelector() {
return createDeepEqualSelector(
createUnoptimizedSelector(),
(indexers) => indexers
);
}
export default function IndexerIndexFooter() {
const indexers = useSelector(createIndexersSelector());
const count = indexers.length;
let enabled = 0;
let torrent = 0;
indexers.forEach((s) => {
if (s.enable) {
enabled += 1;
}
if (s.protocol === 'torrent') {
torrent++;
}
});
return (
<ColorImpairedConsumer>
{(enableColorImpairedMode) => {
return (
<div className={styles.footer}>
<div>
<div className={styles.legendItem}>
<div className={styles.enabled} />
<div>{translate('Enabled')}</div>
</div>
<div className={styles.legendItem}>
<div className={styles.redirected} />
<div>{translate('EnabledRedirected')}</div>
</div>
<div className={styles.legendItem}>
<div className={styles.disabled} />
<div>{translate('Disabled')}</div>
</div>
<div className={styles.legendItem}>
<div
className={classNames(
styles.error,
enableColorImpairedMode && 'colorImpaired'
)}
/>
<div>{translate('Error')}</div>
</div>
</div>
<div className={styles.statistics}>
<DescriptionList>
<DescriptionListItem
title={translate('Indexers')}
data={count}
/>
<DescriptionListItem
title={translate('Enabled')}
data={enabled}
/>
</DescriptionList>
<DescriptionList>
<DescriptionListItem
title={translate('Torrent')}
data={torrent}
/>
<DescriptionListItem
title={translate('Usenet')}
data={count - torrent}
/>
</DescriptionList>
</div>
</div>
);
}}
</ColorImpairedConsumer>
);
}
@@ -1,49 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import IndexerIndexFooter from './IndexerIndexFooter';
function createUnoptimizedSelector() {
return createSelector(
createClientSideCollectionSelector('indexers', 'indexerIndex'),
(indexers) => {
return indexers.items.map((s) => {
const {
protocol,
privacy,
enable
} = s;
return {
protocol,
privacy,
enable
};
});
}
);
}
function createMoviesSelector() {
return createDeepEqualSelector(
createUnoptimizedSelector(),
(indexers) => indexers
);
}
function createMapStateToProps() {
return createSelector(
createMoviesSelector(),
createUISettingsSelector(),
(indexers, uiSettings) => {
return {
indexers,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
}
export default connect(createMapStateToProps)(IndexerIndexFooter);
@@ -1,91 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { executeCommand } from 'Store/Actions/commandActions';
import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
function selectShowSearchAction() {
return createSelector(
(state) => state.indexerIndex,
(indexerIndex) => {
return indexerIndex.tableOptions.showSearchAction;
}
);
}
function createMapStateToProps() {
return createSelector(
createIndexerSelector(),
createIndexerAppProfileSelector(),
createIndexerStatusSelector(),
selectShowSearchAction(),
createUISettingsSelector(),
(
movie,
appProfile,
status,
showSearchAction,
uiSettings
) => {
// If a movie is deleted this selector may fire before the parent
// selecors, which will result in an undefined movie, if that happens
// we want to return early here and again in the render function to avoid
// trying to show a movie that has no information available.
if (!movie) {
return {};
}
return {
...movie,
appProfile,
status,
showSearchAction,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
const mapDispatchToProps = {
dispatchExecuteCommand: executeCommand
};
class IndexerIndexItemConnector extends Component {
//
// Render
render() {
const {
id,
component: ItemComponent,
...otherProps
} = this.props;
if (!id) {
return null;
}
return (
<ItemComponent
{...otherProps}
id={id}
/>
);
}
}
IndexerIndexItemConnector.propTypes = {
id: PropTypes.number,
component: PropTypes.elementType.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerIndexItemConnector);
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
import IndexerIndexFilterModalConnector from 'Indexer/Index/IndexerIndexFilterModalConnector';
import IndexerIndexFilterModal from 'Indexer/Index/IndexerIndexFilterModal';
function IndexerIndexFilterMenu(props) {
const {
@@ -10,7 +10,7 @@ function IndexerIndexFilterMenu(props) {
filters,
customFilters,
isDisabled,
onFilterSelect
onFilterSelect,
} = props;
return (
@@ -20,22 +20,23 @@ function IndexerIndexFilterMenu(props) {
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={IndexerIndexFilterModalConnector}
filterModalConnectorComponent={IndexerIndexFilterModal}
onFilterSelect={onFilterSelect}
/>
);
}
IndexerIndexFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired
onFilterSelect: PropTypes.func.isRequired,
};
IndexerIndexFilterMenu.defaultProps = {
showCustomFilters: false
showCustomFilters: false,
};
export default IndexerIndexFilterMenu;
@@ -7,18 +7,10 @@ import { align, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function IndexerIndexSortMenu(props) {
const {
sortKey,
sortDirection,
isDisabled,
onSortSelect
} = props;
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
return (
<SortMenu
isDisabled={isDisabled}
alignMenu={align.RIGHT}
>
<SortMenu isDisabled={isDisabled} alignMenu={align.RIGHT}>
<MenuContent>
<SortMenuItem
name="status"
@@ -91,7 +83,7 @@ IndexerIndexSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired
onSortSelect: PropTypes.func.isRequired,
};
export default IndexerIndexSortMenu;
@@ -0,0 +1,24 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
interface DeleteIndexerModalProps {
isOpen: boolean;
indexerIds: number[];
onModalClose(): void;
}
function DeleteIndexerModal(props: DeleteIndexerModalProps) {
const { isOpen, indexerIds, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<DeleteIndexerModalContent
indexerIds={indexerIds}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default DeleteIndexerModal;
@@ -0,0 +1,13 @@
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.pathContainer {
margin-left: 5px;
}
.path {
margin-left: 5px;
color: var(--dangerColor);
}
@@ -0,0 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
'path': string;
'pathContainer': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,74 @@
import { orderBy } from 'lodash';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'Components/Link/Button';
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 { kinds } from 'Helpers/Props';
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import styles from './DeleteIndexerModalContent.css';
interface DeleteIndexerModalContentProps {
indexerIds: number[];
onModalClose(): void;
}
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerIds, onModalClose } = props;
const allIndexer = useSelector(createAllIndexersSelector());
const dispatch = useDispatch();
const indexers = useMemo(() => {
const indexers = indexerIds.map((id) => {
return allIndexer.find((s) => s.id === id);
});
return orderBy(indexers, ['sortTitle']);
}, [indexerIds, allIndexer]);
const onDeleteIndexerConfirmed = useCallback(() => {
dispatch(
bulkDeleteIndexers({
indexerIds,
})
);
onModalClose();
}, [indexerIds, dispatch, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Delete Selected Indexer</ModalHeader>
<ModalBody>
<div className={styles.message}>
{`Are you sure you want to delete ${indexers.length} selected indexers?`}
</div>
<ul>
{indexers.map((s) => {
return (
<li key={s.name}>
<span>{s.name}</span>
</li>
);
})}
</ul>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button kind={kinds.DANGER} onPress={onDeleteIndexerConfirmed}>
Delete
</Button>
</ModalFooter>
</ModalContent>
);
}
export default DeleteIndexerModalContent;
@@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditIndexerModalContent from './EditIndexerModalContent';
interface EditIndexerModalProps {
isOpen: boolean;
indexerIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function EditIndexerModal(props: EditIndexerModalProps) {
const { isOpen, indexerIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<EditIndexerModalContent
indexerIds={indexerIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default EditIndexerModal;
@@ -0,0 +1,16 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}
@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'modalFooter': string;
'selected': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,130 @@
import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
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 translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
interface SavePayload {
enable?: boolean;
appProfileId?: number;
}
interface EditIndexerModalContentProps {
indexerIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'true', value: translate('Enabled') },
{ key: 'false', value: translate('Disabled') },
];
function EditIndexerModalContent(props: EditIndexerModalContentProps) {
const { indexerIds, onSavePress, onModalClose } = props;
const [enable, setEnable] = useState(NO_CHANGE);
const [appProfileId, setAppProfileId] = useState<string | number>(NO_CHANGE);
const save = useCallback(() => {
let hasChanges = false;
const payload: SavePayload = {};
if (enable !== NO_CHANGE) {
hasChanges = true;
payload.enable = enable === 'true';
}
if (appProfileId !== NO_CHANGE) {
hasChanges = true;
payload.appProfileId = appProfileId as number;
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [enable, appProfileId, onSavePress, onModalClose]);
const onInputChange = useCallback(
({ name, value }) => {
switch (name) {
case 'enable':
setEnable(value);
break;
case 'appProfileId':
setAppProfileId(value);
break;
default:
console.warn('EditIndexerModalContent Unknown Input');
}
},
[setEnable]
);
const onSavePressWrapper = useCallback(() => {
save();
}, [save]);
const selectedCount = indexerIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('Edit Selected Indexer')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enable"
value={enable}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SyncProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.APP_PROFILE_SELECT}
name="appProfileId"
value={appProfileId}
includeNoChange={true}
includeNoChangeDisabled={false}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('{0} indexers selected', selectedCount.toString())}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={onSavePressWrapper}>
{translate('Apply Changes')}
</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default EditIndexerModalContent;
@@ -0,0 +1,42 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
interface IndexerIndexSelectAllButtonProps {
label: string;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
}
function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
const { isSelectMode } = props;
const [selectState, selectDispatch] = useSelect();
const { allSelected, allUnselected } = selectState;
let icon = icons.SQUARE_MINUS;
if (allSelected) {
icon = icons.CHECK_SQUARE;
} else if (allUnselected) {
icon = icons.SQUARE;
}
const onPress = useCallback(() => {
selectDispatch({
type: allSelected
? SelectActionType.UnselectAll
: SelectActionType.SelectAll,
});
}, [allSelected, selectDispatch]);
return isSelectMode ? (
<PageToolbarButton
label={allSelected ? 'Unselect All' : 'Select All'}
iconName={icon}
onPress={onPress}
/>
) : null;
}
export default IndexerIndexSelectAllButton;
@@ -0,0 +1,43 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
import { icons } from 'Helpers/Props';
interface IndexerIndexSelectAllMenuItemProps {
label: string;
isSelectMode: boolean;
}
function IndexerIndexSelectAllMenuItem(
props: IndexerIndexSelectAllMenuItemProps
) {
const { isSelectMode } = props;
const [selectState, selectDispatch] = useSelect();
const { allSelected, allUnselected } = selectState;
let iconName = icons.SQUARE_MINUS;
if (allSelected) {
iconName = icons.CHECK_SQUARE;
} else if (allUnselected) {
iconName = icons.SQUARE;
}
const onPressWrapper = useCallback(() => {
selectDispatch({
type: allSelected
? SelectActionType.UnselectAll
: SelectActionType.SelectAll,
});
}, [allSelected, selectDispatch]);
return isSelectMode ? (
<PageToolbarOverflowMenuItem
label={allSelected ? 'Unselect All' : 'Select All'}
iconName={iconName}
onPress={onPressWrapper}
/>
) : null;
}
export default IndexerIndexSelectAllMenuItem;
@@ -0,0 +1,72 @@
.footer {
composes: contentFooter from '~Components/Page/PageContentFooter.css';
align-items: center;
}
.buttons {
display: flex;
}
.actionButtons,
.deleteButtons {
display: flex;
gap: 10px;
}
.deleteButtons {
margin-left: 50px;
}
.selected {
display: flex;
justify-content: flex-end;
flex-grow: 1;
font-weight: bold;
}
@media only screen and (max-width: $breakpointMedium) {
.buttons {
justify-content: center;
width: 100%;
}
.selected {
justify-content: center;
margin-bottom: 20px;
width: 100%;
order: -1;
}
}
@media only screen and (max-width: $breakpointSmall) {
.footer {
display: flex;
flex-direction: column;
}
.buttons {
flex-direction: column;
margin-top: 20px;
gap: 20px;
}
.actionButtons {
flex-wrap: wrap;
}
.actionButtons,
.deleteButtons {
display: flex;
justify-content: center;
}
.deleteButtons {
margin-left: 0;
}
.selected {
justify-content: center;
order: -1;
}
}
@@ -0,0 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actionButtons': string;
'buttons': string;
'deleteButtons': string;
'footer': string;
'selected': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,180 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { SelectActionType, useSelect } from 'App/SelectContext';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import DeleteIndexerModal from './Delete/DeleteIndexerModal';
import EditIndexerModal from './Edit/EditIndexerModal';
import TagsModal from './Tags/TagsModal';
import styles from './IndexerIndexSelectFooter.css';
const seriesEditorSelector = createSelector(
(state) => state.indexers,
(indexers) => {
const { isSaving, isDeleting, deleteError } = indexers;
return {
isSaving,
isDeleting,
deleteError,
};
}
);
function IndexerIndexSelectFooter() {
const { isSaving, isDeleting, deleteError } =
useSelector(seriesEditorSelector);
const dispatch = useDispatch();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isSavingIndexer, setIsSavingIndexer] = useState(false);
const [isSavingTags, setIsSavingTags] = useState(false);
const [selectState, selectDispatch] = useSelect();
const { selectedState } = selectState;
const indexerIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const selectedCount = indexerIds.length;
const onEditPress = useCallback(() => {
setIsEditModalOpen(true);
}, [setIsEditModalOpen]);
const onEditModalClose = useCallback(() => {
setIsEditModalOpen(false);
}, [setIsEditModalOpen]);
const onSavePress = useCallback(
(payload) => {
setIsSavingIndexer(true);
setIsEditModalOpen(false);
dispatch(
saveIndexerEditor({
...payload,
indexerIds,
})
);
},
[indexerIds, dispatch]
);
const onTagsPress = useCallback(() => {
setIsTagsModalOpen(true);
}, [setIsTagsModalOpen]);
const onTagsModalClose = useCallback(() => {
setIsTagsModalOpen(false);
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
(tags, applyTags) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);
dispatch(
saveIndexerEditor({
indexerIds,
tags,
applyTags,
})
);
},
[indexerIds, dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
const onDeleteModalClose = useCallback(() => {
setIsDeleteModalOpen(false);
}, []);
useEffect(() => {
if (!isSaving) {
setIsSavingIndexer(false);
setIsSavingTags(false);
}
}, [isSaving]);
useEffect(() => {
if (!isDeleting && !deleteError) {
selectDispatch({ type: SelectActionType.UnselectAll });
}
}, [isDeleting, deleteError, selectDispatch]);
const anySelected = selectedCount > 0;
return (
<PageContentFooter className={styles.footer}>
<div className={styles.buttons}>
<div className={styles.actionButtons}>
<SpinnerButton
isSpinning={isSaving && isSavingIndexer}
isDisabled={!anySelected}
onPress={onEditPress}
>
{translate('Edit')}
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving && isSavingTags}
isDisabled={!anySelected}
onPress={onTagsPress}
>
{translate('Set Tags')}
</SpinnerButton>
</div>
<div className={styles.deleteButtons}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!anySelected || isDeleting}
onPress={onDeletePress}
>
{translate('Delete')}
</SpinnerButton>
</div>
</div>
<div className={styles.selected}>
{translate('{0} indexers selected', selectedCount.toString())}
</div>
<EditIndexerModal
isOpen={isEditModalOpen}
indexerIds={indexerIds}
onSavePress={onSavePress}
onModalClose={onEditModalClose}
/>
<TagsModal
isOpen={isTagsModalOpen}
indexerIds={indexerIds}
onApplyTagsPress={onApplyTagsPress}
onModalClose={onTagsModalClose}
/>
<DeleteIndexerModal
isOpen={isDeleteModalOpen}
indexerIds={indexerIds}
onModalClose={onDeleteModalClose}
/>
</PageContentFooter>
);
}
export default IndexerIndexSelectFooter;
@@ -0,0 +1,39 @@
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
interface IndexerIndexSelectModeButtonProps {
label: string;
iconName: IconDefinition;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
onPress: () => void;
}
function IndexerIndexSelectModeButton(
props: IndexerIndexSelectModeButtonProps
) {
const { label, iconName, isSelectMode, onPress } = props;
const [, selectDispatch] = useSelect();
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: SelectActionType.Reset,
});
}
onPress();
}, [isSelectMode, onPress, selectDispatch]);
return (
<PageToolbarButton
label={label}
iconName={iconName}
onPress={onPressWrapper}
/>
);
}
export default IndexerIndexSelectModeButton;
@@ -0,0 +1,38 @@
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
interface IndexerIndexSelectModeMenuItemProps {
label: string;
iconName: IconDefinition;
isSelectMode: boolean;
onPress: () => void;
}
function IndexerIndexSelectModeMenuItem(
props: IndexerIndexSelectModeMenuItemProps
) {
const { label, iconName, isSelectMode, onPress } = props;
const [, selectDispatch] = useSelect();
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: SelectActionType.Reset,
});
}
onPress();
}, [isSelectMode, onPress, selectDispatch]);
return (
<PageToolbarOverflowMenuItem
label={label}
iconName={iconName}
onPress={onPressWrapper}
/>
);
}
export default IndexerIndexSelectModeMenuItem;
@@ -0,0 +1,22 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import TagsModalContent from './TagsModalContent';
interface TagsModalProps {
isOpen: boolean;
indexerIds: number[];
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModal(props: TagsModalProps) {
const { isOpen, onModalClose, ...otherProps } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<TagsModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default TagsModal;
@@ -0,0 +1,12 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}
@@ -0,0 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
'renameIcon': string;
'result': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,167 @@
import { concat, uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
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, kinds, sizes } from 'Helpers/Props';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import styles from './TagsModalContent.css';
interface TagsModalContentProps {
indexerIds: number[];
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModalContent(props: TagsModalContentProps) {
const { indexerIds, onModalClose, onApplyTagsPress } = props;
const allIndexers = useSelector(createAllIndexersSelector());
const tagList = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
const indexerTags = useMemo(() => {
const indexers = indexerIds.map((id) => {
return allIndexers.find((s) => s.id === id);
});
return uniq(concat(...indexers.map((s) => s.tags)));
}, [indexerIds, allIndexers]);
const onTagsChange = useCallback(
({ value }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
({ value }) => {
setApplyTags(value);
},
[setApplyTags]
);
const onApplyPress = useCallback(() => {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: 'Add' },
{ key: 'remove', value: 'Remove' },
{ key: 'replace', value: 'Replace' },
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Tags</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
value={tags}
onChange={onTagsChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Apply Tags</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="applyTags"
value={applyTags}
values={applyTagsOptions}
helpTexts={[
'How to apply tags to the selected series',
'Add: Add the tags the existing list of tags',
'Remove: Remove the entered tags',
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
]}
onChange={onApplyTagsChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Result</FormLabel>
<div className={styles.result}>
{indexerTags.map((id) => {
const tag = tagList.find((t) => t.id === id);
if (!tag) {
return null;
}
const removeTag =
(applyTags === 'remove' && tags.indexOf(id) > -1) ||
(applyTags === 'replace' && tags.indexOf(id) === -1);
return (
<Label
key={tag.id}
title={removeTag ? 'Removing tag' : 'Existing tag'}
kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})}
{(applyTags === 'add' || applyTags === 'replace') &&
tags.map((id) => {
const tag = tagList.find((t) => t.id === id);
if (!tag) {
return null;
}
if (indexerTags.indexOf(id) > -1) {
return null;
}
return (
<Label
key={tag.id}
title={'Adding tag'}
kind={kinds.SUCCESS}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})}
</div>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
Apply
</Button>
</ModalFooter>
</ModalContent>
);
}
export default TagsModalContent;
@@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
function CapabilitiesLabel(props) {
const {
categoryFilter
} = props;
const {
categories
} = props.capabilities;
let filteredList = categories.filter((item) => item.id < 100000);
if (categoryFilter.length > 0) {
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
}
const nameList = filteredList.map((item) => item.name).sort();
return (
<span>
{
nameList.map((category) => {
return (
<Label key={category}>
{category}
</Label>
);
})
}
{
filteredList.length === 0 ?
<Label>
{'None'}
</Label> :
null
}
</span>
);
}
CapabilitiesLabel.propTypes = {
capabilities: PropTypes.object.isRequired,
categoryFilter: PropTypes.arrayOf(PropTypes.number).isRequired
};
CapabilitiesLabel.defaultProps = {
capabilities: {
categories: []
},
categoryFilter: []
};
export default CapabilitiesLabel;
@@ -0,0 +1,39 @@
import React from 'react';
import Label from 'Components/Label';
import { IndexerCapabilities } from 'Indexer/Indexer';
interface CapabilitiesLabelProps {
capabilities: IndexerCapabilities;
categoryFilter?: number[];
}
function CapabilitiesLabel(props: CapabilitiesLabelProps) {
const { categoryFilter = [] } = props;
const { categories = [] } = props.capabilities;
let filteredList = categories.filter((item) => item.id < 100000);
if (categoryFilter.length > 0) {
filteredList = filteredList.filter(
(item) =>
categoryFilter.includes(item.id) ||
(item.subCategories &&
item.subCategories.some((r) => categoryFilter.includes(r.id)))
);
}
const nameList = filteredList.map((item) => item.name).sort();
return (
<span>
{nameList.map((category) => {
return <Label key={category}>{category}</Label>;
})}
{filteredList.length === 0 ? <Label>{'None'}</Label> : null}
</span>
);
}
export default CapabilitiesLabel;
@@ -1,103 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Indexer/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Indexer/Edit/EditMovieModalConnector';
import translate from 'Utilities/String/translate';
class IndexerIndexActionsCell extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false
};
}
//
// Listeners
onEditMoviePress = () => {
this.setState({ isEditMovieModalOpen: true });
};
onEditMovieModalClose = () => {
this.setState({ isEditMovieModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: true
});
};
onDeleteMovieModalClose = () => {
this.setState({ isDeleteMovieModalOpen: false });
};
//
// Render
render() {
const {
id,
isRefreshingMovie,
onRefreshMoviePress,
...otherProps
} = this.props;
const {
isEditMovieModalOpen,
isDeleteMovieModalOpen
} = this.state;
return (
<VirtualTableRowCell
{...otherProps}
>
<SpinnerIconButton
name={icons.REFRESH}
title={translate('RefreshMovie')}
isSpinning={isRefreshingMovie}
onPress={onRefreshMoviePress}
/>
<IconButton
name={icons.EDIT}
title={translate('EditIndexer')}
onPress={this.onEditMoviePress}
/>
<EditMovieModalConnector
isOpen={isEditMovieModalOpen}
indexerId={id}
onModalClose={this.onEditMovieModalClose}
onDeleteMoviePress={this.onDeleteMoviePress}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
indexerId={id}
onModalClose={this.onDeleteMovieModalClose}
/>
</VirtualTableRowCell>
);
}
}
IndexerIndexActionsCell.propTypes = {
id: PropTypes.number.isRequired,
isRefreshingMovie: PropTypes.bool.isRequired,
onRefreshMoviePress: PropTypes.func.isRequired
};
export default IndexerIndexActionsCell;
@@ -1,132 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import IndexerIndexTableOptionsConnector from './IndexerIndexTableOptionsConnector';
import styles from './IndexerIndexHeader.css';
class IndexerIndexHeader extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isTableOptionsModalOpen: false
};
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
};
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
};
//
// Render
render() {
const {
columns,
onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
isMovieEditorActive,
...otherProps
} = this.props;
return (
<VirtualTableHeader>
{
columns.map((column) => {
const {
name,
label,
isSortable,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'select') {
if (isMovieEditorActive) {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
{...otherProps}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
onPress={this.onTableOptionsPress}
/>
</VirtualTableHeaderCell>
);
}
return (
<VirtualTableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={isSortable}
{...otherProps}
>
{label}
</VirtualTableHeaderCell>
);
})
}
<TableOptionsModal
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
optionsComponent={IndexerIndexTableOptionsConnector}
onTableOptionChange={onTableOptionChange}
onModalClose={this.onTableOptionsModalClose}
/>
</VirtualTableHeader>
);
}
}
IndexerIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isMovieEditorActive: PropTypes.bool.isRequired
};
export default IndexerIndexHeader;
@@ -1,13 +0,0 @@
import { connect } from 'react-redux';
import { setMovieTableOption } from 'Store/Actions/indexerIndexActions';
import IndexerIndexHeader from './IndexerIndexHeader';
function createMapDispatchToProps(dispatch, props) {
return {
onTableOptionChange(payload) {
dispatch(setMovieTableOption(payload));
}
};
}
export default connect(undefined, createMapDispatchToProps)(IndexerIndexHeader);
@@ -0,0 +1,19 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
'added': string;
'appProfileId': string;
'capabilities': string;
'cell': string;
'checkInput': string;
'externalLink': string;
'priority': string;
'privacy': string;
'protocol': string;
'sortName': string;
'status': string;
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,323 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import IndexerInfoModal from 'Indexer/Info/IndexerInfoModal';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
import IndexerStatusCell from './IndexerStatusCell';
import ProtocolLabel from './ProtocolLabel';
import styles from './IndexerIndexRow.css';
class IndexerIndexRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditIndexerModalOpen: false,
isDeleteMovieModalOpen: false,
isIndexerInfoModalOpen: false
};
}
onEditIndexerPress = () => {
this.setState({ isEditIndexerModalOpen: true });
};
onIndexerInfoPress = () => {
this.setState({ isIndexerInfoModalOpen: true });
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onIndexerInfoModalClose = () => {
this.setState({ isIndexerInfoModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditIndexerModalOpen: false,
isDeleteMovieModalOpen: true
});
};
onDeleteMovieModalClose = () => {
this.setState({ isDeleteMovieModalOpen: false });
};
onUseSceneNumberingChange = () => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
//
};
//
// Render
render() {
const {
id,
name,
indexerUrls,
enable,
redirect,
tags,
protocol,
privacy,
priority,
status,
fields,
appProfile,
added,
capabilities,
columns,
longDateFormat,
timeFormat,
isMovieEditorActive,
isSelected,
onSelectedChange
} = this.props;
const {
isEditIndexerModalOpen,
isDeleteMovieModalOpen,
isIndexerInfoModalOpen
} = this.state;
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
return (
<>
{
columns.map((column) => {
const {
isVisible
} = column;
if (!isVisible) {
return null;
}
if (isMovieEditorActive && column.name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={column.name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (column.name === 'status') {
return (
<IndexerStatusCell
key={column.name}
className={styles[column.name]}
enabled={enable}
redirect={redirect}
status={status}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
component={VirtualTableRowCell}
/>
);
}
if (column.name === 'sortName') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{name}
</VirtualTableRowCell>
);
}
if (column.name === 'privacy') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<Label>
{titleCase(privacy)}
</Label>
</VirtualTableRowCell>
);
}
if (column.name === 'priority') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
{priority}
</VirtualTableRowCell>
);
}
if (column.name === 'protocol') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<ProtocolLabel
protocol={protocol}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'appProfileId') {
return (
<VirtualTableRowCell
key={name}
className={styles[column.name]}
>
{appProfile?.name || ''}
</VirtualTableRowCell>
);
}
if (column.name === 'capabilities') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<CapabilitiesLabel
capabilities={capabilities}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'added') {
return (
<RelativeDateCellConnector
key={column.name}
className={styles[column.name]}
date={added}
component={VirtualTableRowCell}
/>
);
}
if (column.name === 'tags') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<TagListConnector
tags={tags}
/>
</VirtualTableRowCell>
);
}
if (column.name === 'actions') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<IconButton
name={icons.INFO}
title={translate('IndexerInfo')}
onPress={this.onIndexerInfoPress}
/>
{
baseUrl ?
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('Website')}
to={baseUrl.replace(/(:\/\/)api\./, '$1')}
/> : null
}
<IconButton
name={icons.EDIT}
title={translate('EditIndexer')}
onPress={this.onEditIndexerPress}
/>
</VirtualTableRowCell>
);
}
return null;
})
}
<EditIndexerModalConnector
id={id}
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
onDeleteIndexerPress={this.onDeleteMoviePress}
/>
<IndexerInfoModal
indexerId={id}
isOpen={isIndexerInfoModalOpen}
onModalClose={this.onIndexerInfoModalClose}
/>
<DeleteIndexerModal
isOpen={isDeleteMovieModalOpen}
indexerId={id}
onModalClose={this.onDeleteMovieModalClose}
/>
</>
);
}
}
IndexerIndexRow.propTypes = {
id: PropTypes.number.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string),
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enable: PropTypes.bool.isRequired,
redirect: PropTypes.bool.isRequired,
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
appProfile: PropTypes.object.isRequired,
status: PropTypes.object,
capabilities: PropTypes.object,
added: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isMovieEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
IndexerIndexRow.defaultProps = {
tags: []
};
export default IndexerIndexRow;
@@ -0,0 +1,256 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector';
import IndexerInfoModal from 'Indexer/Info/IndexerInfoModal';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
import IndexerStatusCell from './IndexerStatusCell';
import ProtocolLabel from './ProtocolLabel';
import styles from './IndexerIndexRow.css';
interface IndexerIndexRowProps {
indexerId: number;
sortKey: string;
columns: Column[];
isSelectMode: boolean;
}
function IndexerIndexRow(props: IndexerIndexRowProps) {
const { indexerId, columns, isSelectMode } = props;
const { indexer, appProfile } = useSelector(
createIndexerIndexItemSelector(props.indexerId)
);
const {
name: indexerName,
indexerUrls,
enable,
redirect,
tags,
protocol,
privacy,
priority,
status,
fields,
added,
capabilities,
} = indexer;
const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ??
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const [selectState, selectDispatch] = useSelect();
const onEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, [setIsEditIndexerModalOpen]);
const onEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
const onIndexerInfoPress = useCallback(() => {
setIsIndexerInfoModalOpen(true);
}, [setIsIndexerInfoModalOpen]);
const onIndexerInfoModalClose = useCallback(() => {
setIsIndexerInfoModalOpen(false);
}, [setIsIndexerInfoModalOpen]);
const onDeleteIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(false);
setIsDeleteIndexerModalOpen(true);
}, [setIsDeleteIndexerModalOpen]);
const onDeleteIndexerModalClose = useCallback(() => {
setIsDeleteIndexerModalOpen(false);
}, [setIsDeleteIndexerModalOpen]);
const checkInputCallback = useCallback(() => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
}, []);
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
selectDispatch({
type: SelectActionType.ToggleSelected,
id,
isSelected: value,
shiftKey,
});
},
[selectDispatch]
);
return (
<>
{isSelectMode ? (
<VirtualTableSelectCell
id={indexerId}
isSelected={selectState.selectedState[indexerId]}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
) : null}
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<IndexerStatusCell
key={name}
className={styles[name]}
enabled={enable}
redirect={redirect}
status={status}
component={VirtualTableRowCell}
/>
);
}
if (name === 'sortName') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{indexerName}
</VirtualTableRowCell>
);
}
if (name === 'privacy') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<Label>{titleCase(privacy)}</Label>
</VirtualTableRowCell>
);
}
if (name === 'priority') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{priority}
</VirtualTableRowCell>
);
}
if (name === 'protocol') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<ProtocolLabel protocol={protocol} />
</VirtualTableRowCell>
);
}
if (name === 'appProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{appProfile?.name || ''}
</VirtualTableRowCell>
);
}
if (name === 'capabilities') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<CapabilitiesLabel capabilities={capabilities} />
</VirtualTableRowCell>
);
}
if (name === 'added') {
return (
<RelativeDateCellConnector
key={name}
className={styles[name]}
date={added.toString()}
component={VirtualTableRowCell}
/>
);
}
if (name === 'tags') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<TagListConnector tags={tags} />
</VirtualTableRowCell>
);
}
if (name === 'actions') {
return (
<VirtualTableRowCell
key={column.name}
className={styles[column.name]}
>
<IconButton
name={icons.INFO}
title={translate('IndexerInfo')}
onPress={onIndexerInfoPress}
/>
{baseUrl ? (
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('Website')}
to={baseUrl.replace('api.', '')}
/>
) : null}
<IconButton
name={icons.EDIT}
title={translate('EditIndexer')}
onPress={onEditIndexerPress}
/>
</VirtualTableRowCell>
);
}
return null;
})}
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
id={indexerId}
onModalClose={onEditIndexerModalClose}
onDeleteIndexerPress={onDeleteIndexerPress}
/>
<IndexerInfoModal
indexerId={indexerId}
isOpen={isIndexerInfoModalOpen}
onModalClose={onIndexerInfoModalClose}
/>
<DeleteIndexerModal
isOpen={isDeleteIndexerModalOpen}
indexerId={indexerId}
onModalClose={onDeleteIndexerModalClose}
/>
</>
);
}
export default IndexerIndexRow;
@@ -1,5 +1,3 @@
.tableContainer {
composes: tableContainer from '~Components/Table/VirtualTable.css';
flex: 1 0 auto;
.tableScroller {
position: relative;
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'tableScroller': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,140 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import { sortDirections } from 'Helpers/Props';
import IndexerIndexItemConnector from 'Indexer/Index/IndexerIndexItemConnector';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import IndexerIndexHeaderConnector from './IndexerIndexHeaderConnector';
import IndexerIndexRow from './IndexerIndexRow';
import styles from './IndexerIndexTable.css';
class IndexerIndexTable extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
scrollIndex: null
};
}
componentDidUpdate(prevProps) {
const {
items,
jumpToCharacter
} = this.props;
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
if (scrollIndex != null) {
this.setState({ scrollIndex });
}
} else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) {
this.setState({ scrollIndex: null });
}
}
//
// Control
rowRenderer = ({ key, rowIndex, style }) => {
const {
items,
columns,
selectedState,
onSelectedChange,
isMovieEditorActive
} = this.props;
const movie = items[rowIndex];
return (
<VirtualTableRow
key={key}
style={style}
>
<IndexerIndexItemConnector
key={movie.id}
component={IndexerIndexRow}
columns={columns}
indexerId={movie.id}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
</VirtualTableRow>
);
};
//
// Render
render() {
const {
items,
columns,
sortKey,
sortDirection,
isSmallScreen,
onSortPress,
scroller,
allSelected,
allUnselected,
onSelectAllChange,
isMovieEditorActive,
selectedState
} = this.props;
return (
<VirtualTable
className={styles.tableContainer}
items={items}
scrollIndex={this.state.scrollIndex}
isSmallScreen={isSmallScreen}
scroller={scroller}
rowHeight={38}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
<IndexerIndexHeaderConnector
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
isMovieEditorActive={isMovieEditorActive}
/>
}
selectedState={selectedState}
columns={columns}
/>
);
}
}
IndexerIndexTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
jumpToCharacter: PropTypes.string,
isSmallScreen: PropTypes.bool.isRequired,
scroller: PropTypes.instanceOf(Element).isRequired,
onSortPress: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isMovieEditorActive: PropTypes.bool.isRequired
};
export default IndexerIndexTable;
@@ -0,0 +1,213 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import Scroller from 'Components/Scroller/Scroller';
import Column from 'Components/Table/Column';
import useMeasure from 'Helpers/Hooks/useMeasure';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import SortDirection from 'Helpers/Props/SortDirection';
import Indexer from 'Indexer/Indexer';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import IndexerIndexRow from './IndexerIndexRow';
import IndexerIndexTableHeader from './IndexerIndexTableHeader';
import selectTableOptions from './selectTableOptions';
import styles from './IndexerIndexTable.css';
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
const bodyPaddingSmallScreen = parseInt(
dimensions.pageContentBodyPaddingSmallScreen
);
interface RowItemData {
items: Indexer[];
sortKey: string;
columns: Column[];
isSelectMode: boolean;
}
interface IndexerIndexTableProps {
items: Indexer[];
sortKey?: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const columnsSelector = createSelector(
(state) => state.indexerIndex.columns,
(columns) => columns
);
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
const { items, sortKey, columns, isSelectMode } = data;
if (index >= items.length) {
return null;
}
const indexer = items[index];
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
...style,
}}
>
<IndexerIndexRow
indexerId={indexer.id}
sortKey={sortKey}
columns={columns}
isSelectMode={isSelectMode}
/>
</div>
);
};
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;
}
function IndexerIndexTable(props: IndexerIndexTableProps) {
const {
items,
sortKey,
sortDirection,
jumpToCharacter,
isSelectMode,
isSmallScreen,
scrollerRef,
} = props;
const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions);
const listRef: React.MutableRefObject<List> = useRef();
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const rowHeight = useMemo(() => {
return showBanners ? 70 : 38;
}, [showBanners]);
useEffect(() => {
const current = scrollerRef.current as HTMLElement;
if (isSmallScreen) {
setSize({
width: windowWidth,
height: windowHeight,
});
return;
}
if (current) {
const width = current.clientWidth;
const padding =
(isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5;
setSize({
width: width - padding * 2,
height: windowHeight,
});
}
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
const scrollTop =
(isSmallScreen
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
return () => {
handleScroll.cancel();
if (currentScrollListener) {
currentScrollListener.removeEventListener('scroll', handleScroll);
}
};
}, [isSmallScreen, listRef, scrollerRef]);
useEffect(() => {
if (jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (index != null) {
let scrollTop = index * rowHeight;
// If the offset is zero go to the top, otherwise offset
// by the approximate size of the header + padding (37 + 20).
if (scrollTop > 0) {
const offset = 57;
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
return (
<div ref={measureRef}>
<Scroller
className={styles.tableScroller}
scrollDirection={ScrollDirection.Horizontal}
>
<IndexerIndexTableHeader
showBanners={showBanners}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
isSelectMode={isSelectMode}
/>
<List<RowItemData>
ref={listRef}
style={{
width: '100%',
height: '100%',
overflow: 'none',
}}
width={size.width}
height={size.height}
itemCount={items.length}
itemSize={rowHeight}
itemData={{
items,
sortKey,
columns,
isSelectMode,
}}
>
{Row}
</List>
</Scroller>
</div>
);
}
export default IndexerIndexTable;
@@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setMovieSort } from 'Store/Actions/indexerIndexActions';
import IndexerIndexTable from './IndexerIndexTable';
function createMapStateToProps() {
return createSelector(
(state) => state.app.dimensions,
(state) => state.indexerIndex.tableOptions,
(state) => state.indexerIndex.columns,
(dimensions, tableOptions, columns) => {
return {
isSmallScreen: dimensions.isSmallScreen,
showBanners: tableOptions.showBanners,
columns
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onSortPress(sortKey) {
dispatch(setMovieSort({ sortKey }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(IndexerIndexTable);
@@ -0,0 +1,16 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
'added': string;
'appProfileId': string;
'capabilities': string;
'priority': string;
'privacy': string;
'protocol': string;
'sortName': string;
'status': string;
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,110 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import IconButton from 'Components/Link/IconButton';
import Column from 'Components/Table/Column';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
setIndexerSort,
setIndexerTableOption,
} from 'Store/Actions/indexerIndexActions';
import IndexerIndexTableOptions from './IndexerIndexTableOptions';
import styles from './IndexerIndexTableHeader.css';
interface IndexerIndexTableHeaderProps {
showBanners: boolean;
columns: Column[];
sortKey?: string;
sortDirection?: SortDirection;
isSelectMode: boolean;
}
function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
const { columns, sortKey, sortDirection, isSelectMode } = props;
const dispatch = useDispatch();
const [selectState, selectDispatch] = useSelect();
const onSortPress = useCallback(
(value) => {
dispatch(setIndexerSort({ sortKey: value }));
},
[dispatch]
);
const onTableOptionChange = useCallback(
(payload) => {
dispatch(setIndexerTableOption(payload));
},
[dispatch]
);
const onSelectAllChange = useCallback(
({ value }) => {
selectDispatch({
type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll,
});
},
[selectDispatch]
);
return (
<VirtualTableHeader>
{isSelectMode ? (
<VirtualTableSelectAllHeaderCell
allSelected={selectState.allSelected}
allUnselected={selectState.allUnselected}
onSelectAllChange={onSelectAllChange}
/>
) : null}
{columns.map((column) => {
const { name, label, isSortable, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
>
<TableOptionsModalWrapper
columns={columns}
optionsComponent={IndexerIndexTableOptions}
onTableOptionChange={onTableOptionChange}
>
<IconButton name={icons.ADVANCED_SETTINGS} />
</TableOptionsModalWrapper>
</VirtualTableHeaderCell>
);
}
return (
<VirtualTableHeaderCell
key={name}
className={classNames(styles[name])}
name={name}
sortKey={sortKey}
sortDirection={sortDirection}
isSortable={isSortable}
onSortPress={onSortPress}
>
{label}
</VirtualTableHeaderCell>
);
})}
</VirtualTableHeader>
);
}
export default IndexerIndexTableHeader;
@@ -1,77 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class IndexerIndexTableOptions extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
showSearchAction: props.showSearchAction
};
}
componentDidUpdate(prevProps) {
const { showSearchAction } = this.props;
if (showSearchAction !== prevProps.showSearchAction) {
this.setState({
showSearchAction
});
}
}
//
// Listeners
onTableOptionChange = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onTableOptionChange({
tableOptions: {
...this.state,
[name]: value
}
});
});
};
//
// Render
render() {
const {
showSearchAction
} = this.state;
return (
<FormGroup>
<FormLabel>{translate('ShowSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showSearchAction"
value={showSearchAction}
helpText={translate('ShowSearchHelpText')}
onChange={this.onTableOptionChange}
/>
</FormGroup>
);
}
}
IndexerIndexTableOptions.propTypes = {
showSearchAction: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired
};
export default IndexerIndexTableOptions;
@@ -0,0 +1,49 @@
import React, { Fragment, useCallback } from 'react';
import { useSelector } from 'react-redux';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import selectTableOptions from './selectTableOptions';
interface IndexerIndexTableOptionsProps {
onTableOptionChange(...args: unknown[]): unknown;
}
function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
const { onTableOptionChange } = props;
const tableOptions = useSelector(selectTableOptions);
const { showSearchAction } = tableOptions;
const onTableOptionChangeWrapper = useCallback(
({ name, value }) => {
onTableOptionChange({
tableOptions: {
...tableOptions,
[name]: value,
},
});
},
[tableOptions, onTableOptionChange]
);
return (
<Fragment>
<FormGroup>
<FormLabel>Show Search</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showSearchAction"
value={showSearchAction}
helpText="Show search button on hover"
onChange={onTableOptionChangeWrapper}
/>
</FormGroup>
</Fragment>
);
}
export default IndexerIndexTableOptions;
@@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import IndexerIndexTableOptions from './IndexerIndexTableOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.indexerIndex.tableOptions,
(tableOptions) => {
return tableOptions;
}
);
}
export default connect(createMapStateToProps)(IndexerIndexTableOptions);
@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'status': string;
'statusIcon': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,66 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
import styles from './IndexerStatusCell.css';
function IndexerStatusCell(props) {
const {
className,
enabled,
redirect,
status,
longDateFormat,
timeFormat,
component: Component,
...otherProps
} = props;
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
const enableTitle = redirect ? 'Indexer is Enabled, Redirect is Enabled' : 'Indexer is Enabled';
return (
<Component
className={className}
{...otherProps}
>
{
<Icon
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon: icons.BLOCKLIST}
title={enabled ? enableTitle : 'Indexer is Disabled'}
/>
}
{
status &&
<Icon
className={styles.statusIcon}
kind={kinds.DANGER}
name={icons.WARNING}
title={`Indexer is Disabled due to failures until ${formatDateTime(status.disabledTill, longDateFormat, timeFormat)}`}
/>
}
</Component>
);
}
IndexerStatusCell.propTypes = {
className: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
redirect: PropTypes.bool.isRequired,
status: PropTypes.object,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
component: PropTypes.elementType
};
IndexerStatusCell.defaultProps = {
className: styles.status,
component: VirtualTableRowCell
};
export default IndexerStatusCell;
@@ -0,0 +1,57 @@
import React from 'react';
import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import { IndexerStatus } from 'Indexer/Indexer';
import formatDateTime from 'Utilities/Date/formatDateTime';
import styles from './IndexerStatusCell.css';
interface IndexerStatusCellProps {
className: string;
enabled: boolean;
redirect: boolean;
status: IndexerStatus;
component?: React.ElementType;
}
function IndexerStatusCell(props: IndexerStatusCellProps) {
const {
className,
enabled,
redirect,
status,
component: Component = VirtualTableRowCell,
...otherProps
} = props;
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
const enableTitle = redirect
? 'Indexer is Enabled, Redirect is Enabled'
: 'Indexer is Enabled';
return (
<Component className={className} {...otherProps}>
{
<Icon
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon : icons.BLOCKLIST}
title={enabled ? enableTitle : 'Indexer is Disabled'}
/>
}
{status ? (
<Icon
className={styles.statusIcon}
kind={kinds.DANGER}
name={icons.WARNING}
title={`Indexer is Disabled due to failures until ${formatDateTime(
status.disabledTill
)}`}
/>
) : null}
</Component>
);
}
export default IndexerStatusCell;
@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'torrent': string;
'usenet': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,20 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import styles from './ProtocolLabel.css';
function ProtocolLabel({ protocol }) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return (
<Label className={styles[protocol]}>
{protocolName}
</Label>
);
}
ProtocolLabel.propTypes = {
protocol: PropTypes.string.isRequired
};
export default ProtocolLabel;
@@ -0,0 +1,17 @@
import React from 'react';
import Label from 'Components/Label';
import styles from './ProtocolLabel.css';
interface ProtocolLabelProps {
protocol: string;
}
function ProtocolLabel(props: ProtocolLabelProps) {
const { protocol } = props;
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return <Label className={styles[protocol]}>{protocolName}</Label>;
}
export default ProtocolLabel;
@@ -0,0 +1,8 @@
import { createSelector } from 'reselect';
const selectTableOptions = createSelector(
(state) => state.indexerIndex.tableOptions,
(tableOptions) => tableOptions
);
export default selectTableOptions;
@@ -0,0 +1,28 @@
import { createSelector } from 'reselect';
import Indexer from 'Indexer/Indexer';
import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
function createIndexerIndexItemSelector(indexerId: number) {
return createSelector(
createIndexerSelector(indexerId),
createIndexerAppProfileSelector(indexerId),
(indexer: Indexer, appProfile) => {
// If a series is deleted this selector may fire before the parent
// selectors, which will result in an undefined series, if that happens
// we want to return early here and again in the render function to avoid
// trying to show a series that has no information available.
if (!indexer) {
return {};
}
return {
indexer,
appProfile,
};
}
);
}
export default createIndexerIndexItemSelector;