mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
74 Commits
v5.9.1.907
...
v5.10.2.91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2843647e23 | ||
|
|
b6b7d30fc1 | ||
|
|
e688dfadf7 | ||
|
|
f3ce0ac620 | ||
|
|
08c250dfe3 | ||
|
|
a5c1025efd | ||
|
|
f0019d622a | ||
|
|
032e8aa920 | ||
|
|
51cd7c70ba | ||
|
|
12c814ed78 | ||
|
|
d7bdc2c46c | ||
|
|
6b4c0bd24c | ||
|
|
c332c38890 | ||
|
|
d910611630 | ||
|
|
c6feb27962 | ||
|
|
0f7721ef11 | ||
|
|
89731bdc41 | ||
|
|
126b6eba00 | ||
|
|
56fece293c | ||
|
|
4503c3d36e | ||
|
|
0d6ce5ea49 | ||
|
|
b4b5ad9567 | ||
|
|
3ae6347532 | ||
|
|
a826ffdbc9 | ||
|
|
dc3bf9acb0 | ||
|
|
eca95826c2 | ||
|
|
593b943cb0 | ||
|
|
a3faa9ed5f | ||
|
|
229e87f398 | ||
|
|
fcb758bf67 | ||
|
|
0a9ae45ed1 | ||
|
|
b062a46cbd | ||
|
|
ac4669dfc1 | ||
|
|
36d80387c6 | ||
|
|
9fe4793606 | ||
|
|
fa80608394 | ||
|
|
6e81d5917e | ||
|
|
8d189523c4 | ||
|
|
4d589422e6 | ||
|
|
675612e7c6 | ||
|
|
be3916f67d | ||
|
|
453f216e0d | ||
|
|
2d4846e5be | ||
|
|
2700a6cf8a | ||
|
|
674e414111 | ||
|
|
21bd21b70c | ||
|
|
fde87a38f9 | ||
|
|
0d6ba200d3 | ||
|
|
93298645e3 | ||
|
|
58f544e9e0 | ||
|
|
cf952d5c0b | ||
|
|
f6d630bdd3 | ||
|
|
657ced4772 | ||
|
|
d3a0c83f98 | ||
|
|
5833d5d4c4 | ||
|
|
2ba4562f49 | ||
|
|
d79db69644 | ||
|
|
7532dfb03c | ||
|
|
a47528aa81 | ||
|
|
a812d9f39f | ||
|
|
fc97f05850 | ||
|
|
644876123d | ||
|
|
540659a799 | ||
|
|
288668f7e6 | ||
|
|
fcf3be42d5 | ||
|
|
16e218501e | ||
|
|
caf2d33c11 | ||
|
|
bc918ed3b5 | ||
|
|
df77474314 | ||
|
|
bf84471509 | ||
|
|
d346d969de | ||
|
|
14b125ccd9 | ||
|
|
da5323a08f | ||
|
|
672b351497 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -118,6 +118,7 @@ src/UI/.idea/*
|
||||
node_modules/
|
||||
_output*
|
||||
_artifacts
|
||||
_temp*
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.9.1'
|
||||
majorVersion: '5.10.2'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||
|
||||
class Blocklist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmRemoveModalOpen: false,
|
||||
isConfirmClearModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
if (hasDifferentItems(prevProps.items, items)) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = () => {
|
||||
this.props.onRemoveSelected(this.getSelectedIds());
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.setState({ isConfirmClearModalOpen: true });
|
||||
};
|
||||
|
||||
onClearBlocklistConfirmed = () => {
|
||||
this.props.onClearBlocklistPress();
|
||||
this.setState({ isConfirmClearModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmClearModalClose = () => {
|
||||
this.setState({ isConfirmClearModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlocklistExecuting,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isConfirmClearModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Blocklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={this.onClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={BlocklistFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('BlocklistLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey === 'all' ?
|
||||
translate('NoHistoryBlocklist') :
|
||||
translate('BlocklistFilterHasNoItems')
|
||||
}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlocklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('RemoveSelected')}
|
||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmClearModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ClearBlocklist')}
|
||||
message={translate('ClearBlocklistMessageText')}
|
||||
confirmLabel={translate('Clear')}
|
||||
onConfirm={this.onClearBlocklistConfirmed}
|
||||
onCancel={this.onConfirmClearModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Blocklist.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
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,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlocklistPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blocklist;
|
||||
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import usePaging from 'Components/Table/usePaging';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
clearBlocklist,
|
||||
fetchBlocklist,
|
||||
gotoBlocklistPage,
|
||||
removeBlocklistItems,
|
||||
setBlocklistFilter,
|
||||
setBlocklistSort,
|
||||
setBlocklistTableOption,
|
||||
} from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||
import BlocklistRow from './BlocklistRow';
|
||||
|
||||
function Blocklist() {
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
} = useSelector((state: AppState) => state.blocklist);
|
||||
|
||||
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
|
||||
const isClearingBlocklistExecuting = useSelector(
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||
useState(false);
|
||||
const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
|
||||
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const wasClearingBlocklistExecuting = usePrevious(
|
||||
isClearingBlocklistExecuting
|
||||
);
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleRemoveSelectedPress = useCallback(() => {
|
||||
setIsConfirmRemoveModalOpen(true);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const handleRemoveSelectedConfirmed = useCallback(() => {
|
||||
dispatch(removeBlocklistItems({ ids: selectedIds }));
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
|
||||
|
||||
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const handleClearBlocklistPress = useCallback(() => {
|
||||
setIsConfirmClearModalOpen(true);
|
||||
}, [setIsConfirmClearModalOpen]);
|
||||
|
||||
const handleClearBlocklistConfirmed = useCallback(() => {
|
||||
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
|
||||
setIsConfirmClearModalOpen(false);
|
||||
}, [setIsConfirmClearModalOpen, dispatch]);
|
||||
|
||||
const handleConfirmClearModalClose = useCallback(() => {
|
||||
setIsConfirmClearModalOpen(false);
|
||||
}, [setIsConfirmClearModalOpen]);
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoBlocklistPage,
|
||||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string) => {
|
||||
dispatch(setBlocklistSort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setBlocklistTableOption(payload));
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchBlocklist());
|
||||
} else {
|
||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(clearBlocklist());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchBlocklist());
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
|
||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||
}
|
||||
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
|
||||
|
||||
return (
|
||||
<SelectProvider items={items}>
|
||||
<PageContent title={translate('Blocklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
onPress={handleRemoveSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={handleClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={BlocklistFilterModal}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>
|
||||
{selectedFilterKey === 'all'
|
||||
? translate('NoBlocklistItems')
|
||||
: translate('BlocklistFilterHasNoItems')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !!items.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<BlocklistRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePager
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('RemoveSelected')}
|
||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={handleRemoveSelectedConfirmed}
|
||||
onCancel={handleConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmClearModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ClearBlocklist')}
|
||||
message={translate('ClearBlocklistMessageText')}
|
||||
confirmLabel={translate('Clear')}
|
||||
onConfirm={handleClearBlocklistConfirmed}
|
||||
onCancel={handleConfirmClearModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blocklist;
|
||||
@@ -1,161 +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 withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Blocklist from './Blocklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blocklist,
|
||||
createCustomFiltersSelector('blocklist'),
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, customFilters, isClearingBlocklistExecuting) => {
|
||||
return {
|
||||
isClearingBlocklistExecuting,
|
||||
customFilters,
|
||||
...blocklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blocklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlocklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchBlocklist,
|
||||
gotoBlocklistFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchBlocklist();
|
||||
} else {
|
||||
gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearBlocklist();
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlocklist();
|
||||
};
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlocklistPreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlocklistNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlocklistLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlocklistPage({ page });
|
||||
};
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlocklistItems({ ids });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setBlocklistFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Blocklist
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlocklistConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlocklist: PropTypes.func.isRequired,
|
||||
gotoBlocklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||
removeBlocklistItems: PropTypes.func.isRequired,
|
||||
setBlocklistSort: PropTypes.func.isRequired,
|
||||
setBlocklistFilter: PropTypes.func.isRequired,
|
||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlocklist: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
||||
);
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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 translate from 'Utilities/String/translate';
|
||||
|
||||
class BlocklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Details
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Protocol')}
|
||||
data={protocol}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlocklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlocklistDetailsModal;
|
||||
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface BlocklistDetailsModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
protocol: DownloadProtocol;
|
||||
indexer?: string;
|
||||
message?: string;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
|
||||
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Details</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Protocol')}
|
||||
data={protocol}
|
||||
/>
|
||||
|
||||
{message ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{message ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
) : null}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlocklistDetailsModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
.language,
|
||||
.languages,
|
||||
.quality {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'indexer': string;
|
||||
'language': string;
|
||||
'languages': string;
|
||||
'quality': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||
import styles from './BlocklistRow.css';
|
||||
|
||||
class BlocklistRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
movie,
|
||||
sourceTitle,
|
||||
quality,
|
||||
customFormats,
|
||||
languages,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onRemovePress
|
||||
} = this.props;
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.quality}
|
||||
>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRemovePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<BlocklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
indexer={indexer}
|
||||
message={message}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BlocklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movie: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlocklistRow;
|
||||
160
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
160
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||
import styles from './BlocklistRow.css';
|
||||
|
||||
interface BlocklistRowProps extends Blocklist {
|
||||
isSelected: boolean;
|
||||
columns: Column[];
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function BlocklistRow(props: BlocklistRowProps) {
|
||||
const {
|
||||
id,
|
||||
movieId,
|
||||
sourceTitle,
|
||||
languages,
|
||||
quality,
|
||||
customFormats,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
} = props;
|
||||
|
||||
const movie = useMovie(movieId);
|
||||
const dispatch = useDispatch();
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
|
||||
const handleDetailsPress = useCallback(() => {
|
||||
setIsDetailsModalOpen(true);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
const handleDetailsModalClose = useCallback(() => {
|
||||
setIsDetailsModalOpen(false);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
const handleRemovePress = useCallback(() => {
|
||||
dispatch(removeBlocklistItem({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.languages}>
|
||||
<MovieLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.quality}>
|
||||
<MovieQuality quality={quality} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
return <RelativeDateCell key={name} date={date} />;
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.indexer}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.actions}>
|
||||
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
<BlocklistDetailsModal
|
||||
isOpen={isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
indexer={indexer}
|
||||
message={message}
|
||||
onModalClose={handleDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlocklistRow;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import BlocklistRow from './BlocklistRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
(movie) => {
|
||||
return {
|
||||
movie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeBlocklistItem({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
@@ -143,7 +143,7 @@ class HistoryRow extends Component {
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
|
||||
@@ -11,3 +11,7 @@
|
||||
border-color: var(--usenetColor);
|
||||
background-color: var(--usenetColor);
|
||||
}
|
||||
|
||||
.unknown {
|
||||
composes: label from '~Components/Label.css';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'torrent': string;
|
||||
'unknown': string;
|
||||
'usenet': string;
|
||||
}
|
||||
export const cssExports: 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;
|
||||
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import styles from './ProtocolLabel.css';
|
||||
|
||||
interface ProtocolLabelProps {
|
||||
protocol: DownloadProtocol;
|
||||
}
|
||||
|
||||
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
|
||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||
|
||||
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
||||
}
|
||||
|
||||
export default ProtocolLabel;
|
||||
@@ -1,374 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
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 Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._shouldBlockRefresh = false;
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
if (this._shouldBlockRefresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
isMoviesFetching
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(!isMoviesFetching && prevProps.isMoviesFetching) ||
|
||||
(!isFetching && prevProps.isFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
|
||||
) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nextState = {};
|
||||
|
||||
if (prevProps.items !== items) {
|
||||
nextState.items = items;
|
||||
}
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
nextState.isPendingSelected = isPendingSelected;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(nextState)) {
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||
this._shouldBlockRefresh = isOpen;
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onGrabSelectedPress = () => {
|
||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
||||
this._shouldBlockRefresh = true;
|
||||
});
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isMoviesFetching,
|
||||
isMoviesPopulated,
|
||||
moviesError,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
count,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected,
|
||||
items
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
|
||||
const hasError = error || moviesError;
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Queue')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('GrabSelected')}
|
||||
iconName={icons.DOWNLOAD}
|
||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={this.onGrabSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={disableSelectedActions}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
maxPageSize={200}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isRefreshing && hasError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('QueueLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey !== 'all' && count > 0 ?
|
||||
translate('QueueFilterHasNoItems') :
|
||||
translate('QueueIsEmpty')
|
||||
}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !!items.length ?
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueueRowConnector
|
||||
key={item.id}
|
||||
movieId={item.movieId}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isRefreshing}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Queue.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isMoviesFetching: PropTypes.bool.isRequired,
|
||||
isMoviesPopulated: PropTypes.bool.isRequired,
|
||||
moviesError: PropTypes.object,
|
||||
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,
|
||||
count: PropTypes.number.isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Queue.defaultProps = {
|
||||
count: 0
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
400
frontend/src/Activity/Queue/Queue.tsx
Normal file
400
frontend/src/Activity/Queue/Queue.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
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 Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import usePaging from 'Components/Table/usePaging';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
clearQueue,
|
||||
fetchQueue,
|
||||
gotoQueuePage,
|
||||
grabQueueItems,
|
||||
removeQueueItems,
|
||||
setQueueFilter,
|
||||
setQueueSort,
|
||||
setQueueTableOption,
|
||||
} from 'Store/Actions/queueActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptions from './QueueOptions';
|
||||
import QueueRow from './QueueRow';
|
||||
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||
import createQueueStatusSelector from './Status/createQueueStatusSelector';
|
||||
|
||||
function Queue() {
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
} = useSelector((state: AppState) => state.queue.paged);
|
||||
|
||||
const { count } = useSelector(createQueueStatusSelector());
|
||||
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
|
||||
createMoviesFetchingSelector()
|
||||
);
|
||||
const customFilters = useSelector(createCustomFiltersSelector('queue'));
|
||||
|
||||
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
|
||||
);
|
||||
|
||||
const shouldBlockRefresh = useRef(false);
|
||||
const currentQueue = useRef<ReactElement | null>(null);
|
||||
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const isPendingSelected = useMemo(() => {
|
||||
return items.some((item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
}, [items, selectedIds]);
|
||||
|
||||
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const isRefreshing =
|
||||
isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated =
|
||||
isPopulated &&
|
||||
(isMoviesPopulated || !items.length || items.every((m) => !m.movieId));
|
||||
const hasError = error || moviesError;
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
|
||||
shouldBlockRefresh.current = isOpen;
|
||||
}, []);
|
||||
|
||||
const handleGrabSelectedPress = useCallback(() => {
|
||||
dispatch(grabQueueItems({ ids: selectedIds }));
|
||||
}, [selectedIds, dispatch]);
|
||||
|
||||
const handleRemoveSelectedPress = useCallback(() => {
|
||||
shouldBlockRefresh.current = true;
|
||||
setIsConfirmRemoveModalOpen(true);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const handleRemoveSelectedConfirmed = useCallback(
|
||||
(payload: RemovePressProps) => {
|
||||
shouldBlockRefresh.current = false;
|
||||
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
},
|
||||
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
|
||||
);
|
||||
|
||||
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||
shouldBlockRefresh.current = false;
|
||||
setIsConfirmRemoveModalOpen(false);
|
||||
}, [setIsConfirmRemoveModalOpen]);
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoQueuePage,
|
||||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string) => {
|
||||
dispatch(setQueueSort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setQueueTableOption(payload));
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoQueuePage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchQueue());
|
||||
} else {
|
||||
dispatch(gotoQueuePage({ page: 1 }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(clearQueue());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchQueue());
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
if (!shouldBlockRefresh.current) {
|
||||
currentQueue.current = (
|
||||
<PageContentBody>
|
||||
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isRefreshing && hasError ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isAllPopulated && !hasError && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>
|
||||
{selectedFilterKey !== 'all' && count > 0
|
||||
? translate('QueueFilterHasNoItems')
|
||||
: translate('QueueIsEmpty')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isAllPopulated && !hasError && !!items.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
optionsComponent={QueueOptions}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<QueueRow
|
||||
key={item.id}
|
||||
movieId={item.movieId}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
onQueueRowModalOpenOrClose={
|
||||
handleQueueRowModalOpenOrClose
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Queue')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={handleRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('GrabSelected')}
|
||||
iconName={icons.DOWNLOAD}
|
||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={handleGrabSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={disableSelectedActions}
|
||||
isSpinning={isRemoving}
|
||||
onPress={handleRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
maxPageSize={200}
|
||||
optionsComponent={QueueOptions}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
{currentQueue.current}
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={
|
||||
isConfirmRemoveModalOpen &&
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
}
|
||||
canIgnore={
|
||||
isConfirmRemoveModalOpen &&
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
}
|
||||
isPending={
|
||||
isConfirmRemoveModalOpen &&
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
item.status === 'delay' ||
|
||||
item.status === 'downloadClientUnavailable'
|
||||
);
|
||||
})
|
||||
}
|
||||
onRemovePress={handleRemoveSelectedConfirmed}
|
||||
onModalClose={handleConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Queue;
|
||||
@@ -1,185 +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 withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
(state) => state.queue.status.item,
|
||||
createCustomFiltersSelector('queue'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...queueActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class QueueConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchQueue,
|
||||
fetchQueueStatus,
|
||||
gotoQueueFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchQueue();
|
||||
} else {
|
||||
gotoQueueFirstPage();
|
||||
}
|
||||
|
||||
fetchQueueStatus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.includeUnknownMovieItems !==
|
||||
prevProps.includeUnknownMovieItems
|
||||
) {
|
||||
this.repopulate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearQueue();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchQueue();
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoQueueFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoQueuePreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoQueueNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoQueueLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoQueuePage({ page });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setQueueFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
};
|
||||
|
||||
onGrabSelectedPress = (ids) => {
|
||||
this.props.grabQueueItems({ ids });
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Queue
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueConnector.propTypes = {
|
||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchQueue: PropTypes.func.isRequired,
|
||||
fetchQueueStatus: PropTypes.func.isRequired,
|
||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||
gotoQueuePage: PropTypes.func.isRequired,
|
||||
setQueueSort: PropTypes.func.isRequired,
|
||||
setQueueFilter: PropTypes.func.isRequired,
|
||||
setQueueTableOption: PropTypes.func.isRequired,
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
removeQueueItems: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
||||
);
|
||||
@@ -1,36 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import styles from './QueueDetails.css';
|
||||
|
||||
function QueueDetails(props) {
|
||||
interface QueueDetailsProps {
|
||||
title: string;
|
||||
size: number;
|
||||
sizeleft: number;
|
||||
estimatedCompletionTime?: string;
|
||||
status: string;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
progressBar: React.ReactNode;
|
||||
}
|
||||
|
||||
function QueueDetails(props: QueueDetailsProps) {
|
||||
const {
|
||||
title,
|
||||
size,
|
||||
sizeleft,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState = 'downloading',
|
||||
trackedDownloadStatus = 'ok',
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
progressBar
|
||||
progressBar,
|
||||
} = props;
|
||||
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||
const isDownloading = status === 'downloading';
|
||||
const isPaused = status === 'paused';
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
if (
|
||||
(isDownloading || isPaused) &&
|
||||
!hasWarning &&
|
||||
!hasError
|
||||
) {
|
||||
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
|
||||
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||
|
||||
if (progress < 5) {
|
||||
@@ -45,11 +58,9 @@ function QueueDetails(props) {
|
||||
return (
|
||||
<Popover
|
||||
className={styles.progressBarContainer}
|
||||
anchor={progressBar}
|
||||
anchor={progressBar!}
|
||||
title={`${state} - ${progress.toFixed(1)}%`}
|
||||
body={
|
||||
<div>{title}</div>
|
||||
}
|
||||
body={<div>{title}</div>}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
@@ -68,22 +79,4 @@ function QueueDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
QueueDetails.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
QueueDetails.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading'
|
||||
};
|
||||
|
||||
export default QueueDetails;
|
||||
@@ -1,78 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } 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 QueueOptions extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
includeUnknownMovieItems: props.includeUnknownMovieItems
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
includeUnknownMovieItems
|
||||
} = this.props;
|
||||
|
||||
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
|
||||
this.setState({
|
||||
includeUnknownMovieItems
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOptionChange = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onOptionChange({
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
includeUnknownMovieItems
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownMovieItems"
|
||||
value={includeUnknownMovieItems}
|
||||
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||
onChange={this.onOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueOptions.propTypes = {
|
||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||
onOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QueueOptions;
|
||||
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function QueueOptions() {
|
||||
const dispatch = useDispatch();
|
||||
const { includeUnknownMovieItems } = useSelector(
|
||||
(state: AppState) => state.queue.options
|
||||
);
|
||||
|
||||
const handleOptionChange = useCallback(
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
dispatch(
|
||||
setQueueOption({
|
||||
[name]: value,
|
||||
})
|
||||
);
|
||||
|
||||
if (name === 'includeUnknownMovieItems') {
|
||||
dispatch(gotoQueuePage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownMovieItems"
|
||||
value={includeUnknownMovieItems}
|
||||
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueOptions;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||
import QueueOptions from './QueueOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.queue.options,
|
||||
(options) => {
|
||||
return options;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onOptionChange: setQueueOption
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
||||
@@ -1,434 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
import styles from './QueueRow.css';
|
||||
|
||||
class QueueRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRemoveQueueItemModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveQueueItemPress = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
||||
const {
|
||||
onRemoveQueueItemPress,
|
||||
onQueueRowModalOpenOrClose
|
||||
} = this.props;
|
||||
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
onRemoveQueueItemPress(blocklist, skipRedownload);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(true);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
downloadId,
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
movie,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
isRemoving,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isRemoveQueueItemModalOpen,
|
||||
isInteractiveImportModalOpen
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<QueueStatusCell
|
||||
key={name}
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
movie ?
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/> :
|
||||
title
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
quality ?
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.customFormatScore}
|
||||
>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{downloadClient}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'year') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
movie ? movie.year : ''
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{title}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{outputPath}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'estimatedCompletionTime') {
|
||||
return (
|
||||
<TimeleftCell
|
||||
key={name}
|
||||
status={status}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
timeleft={timeleft}
|
||||
size={size}
|
||||
sizeleft={sizeleft}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'progress') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.progress}
|
||||
>
|
||||
{
|
||||
!!progress &&
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
title={`${progress.toFixed(1)}%`}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
showInteractiveImport &&
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPending &&
|
||||
<SpinnerIconButton
|
||||
name={icons.DOWNLOAD}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFromQueue')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
downloadId={downloadId}
|
||||
title={title}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QueueRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadState: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
movie: PropTypes.object,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
year: PropTypes.number,
|
||||
sizeleft: PropTypes.number,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.object,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
||||
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QueueRow.defaultProps = {
|
||||
customFormats: [],
|
||||
isGrabbing: false,
|
||||
isRemoving: false
|
||||
};
|
||||
|
||||
export default QueueRow;
|
||||
361
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
361
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import Language from 'Language/Language';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
import styles from './QueueRow.css';
|
||||
|
||||
interface QueueRowProps {
|
||||
id: number;
|
||||
movieId?: number;
|
||||
downloadId?: string;
|
||||
title: string;
|
||||
status: string;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
protocol: DownloadProtocol;
|
||||
indexer?: string;
|
||||
outputPath?: string;
|
||||
downloadClient?: string;
|
||||
downloadClientHasPostImportCategory?: boolean;
|
||||
estimatedCompletionTime?: string;
|
||||
added?: string;
|
||||
timeleft?: string;
|
||||
size: number;
|
||||
sizeleft: number;
|
||||
isGrabbing?: boolean;
|
||||
grabError?: Error;
|
||||
isRemoving?: boolean;
|
||||
isSelected?: boolean;
|
||||
columns: Column[];
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
function QueueRow(props: QueueRowProps) {
|
||||
const {
|
||||
id,
|
||||
movieId,
|
||||
downloadId,
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
languages,
|
||||
quality,
|
||||
customFormats = [],
|
||||
customFormatScore,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
isGrabbing = false,
|
||||
grabError,
|
||||
isRemoving = false,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onQueueRowModalOpenOrClose,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const movie = useMovie(movieId);
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleGrabPress = useCallback(() => {
|
||||
dispatch(grabQueueItem({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleInteractiveImportPress = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(true);
|
||||
setIsInteractiveImportModalOpen(true);
|
||||
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const handleInteractiveImportModalClose = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
setIsInteractiveImportModalOpen(false);
|
||||
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const handleRemoveQueueItemPress = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(true);
|
||||
setIsRemoveQueueItemModalOpen(true);
|
||||
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const handleRemoveQueueItemModalConfirmed = useCallback(
|
||||
(payload: RemovePressProps) => {
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
dispatch(removeQueueItem({ id, ...payload }));
|
||||
setIsRemoveQueueItemModalOpen(false);
|
||||
},
|
||||
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
|
||||
);
|
||||
|
||||
const handleRemoveQueueItemModalClose = useCallback(() => {
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
setIsRemoveQueueItemModalOpen(false);
|
||||
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||
|
||||
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||
const showInteractiveImport =
|
||||
status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending =
|
||||
status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<QueueStatusCell
|
||||
key={name}
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{movie ? (
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'year') {
|
||||
return (
|
||||
<TableRowCell key={name}>{movie ? movie.year : ''}</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{quality ? <MovieQuality quality={quality} /> : null}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return <TableRowCell key={name}>{indexer}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return <TableRowCell key={name}>{title}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'estimatedCompletionTime') {
|
||||
return (
|
||||
<TimeleftCell
|
||||
key={name}
|
||||
status={status}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
timeleft={timeleft}
|
||||
size={size}
|
||||
sizeleft={sizeleft}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'progress') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.progress}>
|
||||
{!!progress && (
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
title={`${progress.toFixed(1)}%`}
|
||||
/>
|
||||
)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return <RelativeDateCell key={name} date={added} />;
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.actions}>
|
||||
{showInteractiveImport ? (
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={handleInteractiveImportPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isPending ? (
|
||||
<SpinnerIconButton
|
||||
name={icons.DOWNLOAD}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={handleGrabPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFromQueue')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isRemoving}
|
||||
onPress={handleRemoveQueueItemPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
downloadId={downloadId}
|
||||
modalTitle={title}
|
||||
onModalClose={handleInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={handleRemoveQueueItemModalConfirmed}
|
||||
onModalClose={handleRemoveQueueItemModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueRow;
|
||||
@@ -1,67 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueueRow from './QueueRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
createUISettingsSelector(),
|
||||
(movie, uiSettings) => {
|
||||
const result = {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
|
||||
result.movie = movie;
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
grabQueueItem,
|
||||
removeQueueItem
|
||||
};
|
||||
|
||||
class QueueRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
};
|
||||
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QueueRow
|
||||
{...this.props}
|
||||
onGrabPress={this.onGrabPress}
|
||||
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movie: PropTypes.object,
|
||||
grabQueueItem: PropTypes.func.isRequired,
|
||||
removeQueueItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
||||
@@ -1,51 +1,59 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Icon, { IconProps } from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueueStatus.css';
|
||||
|
||||
function getDetailedPopoverBody(statusMessages) {
|
||||
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={messages.length ? undefined: styles.noMessages}
|
||||
>
|
||||
{title}
|
||||
<ul>
|
||||
{
|
||||
messages.map((message) => {
|
||||
return (
|
||||
<li key={message}>
|
||||
{message}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={messages.length ? undefined : styles.noMessages}
|
||||
>
|
||||
{title}
|
||||
<ul>
|
||||
{messages.map((message) => {
|
||||
return <li key={message}>{message}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QueueStatus(props) {
|
||||
interface QueueStatusProps {
|
||||
sourceTitle: string;
|
||||
status: string;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
position: TooltipPosition;
|
||||
canFlip?: boolean;
|
||||
}
|
||||
|
||||
function QueueStatus(props: QueueStatusProps) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
trackedDownloadStatus = 'ok',
|
||||
trackedDownloadState = 'downloading',
|
||||
statusMessages = [],
|
||||
errorMessage,
|
||||
position,
|
||||
canFlip
|
||||
canFlip = false,
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
@@ -53,7 +61,7 @@ function QueueStatus(props) {
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let iconKind: IconProps['kind'] = kinds.DEFAULT;
|
||||
let title = translate('Downloading');
|
||||
|
||||
if (status === 'paused') {
|
||||
@@ -115,7 +123,8 @@ function QueueStatus(props) {
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||
const warningMessage =
|
||||
errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', { warningMessage });
|
||||
}
|
||||
|
||||
@@ -133,35 +142,17 @@ function QueueStatus(props) {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
}
|
||||
anchor={<Icon name={iconName} kind={iconKind} />}
|
||||
title={title}
|
||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||
body={
|
||||
hasWarning || hasError
|
||||
? getDetailedPopoverBody(statusMessages)
|
||||
: sourceTitle
|
||||
}
|
||||
position={position}
|
||||
canFlip={canFlip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatus.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
|
||||
canFlip: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
QueueStatus.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading',
|
||||
canFlip: false
|
||||
};
|
||||
|
||||
export default QueueStatus;
|
||||
@@ -1,47 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { tooltipPositions } from 'Helpers/Props';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.status}>
|
||||
<QueueStatus
|
||||
sourceTitle={sourceTitle}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading'
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import {
|
||||
QueueTrackedDownloadState,
|
||||
QueueTrackedDownloadStatus,
|
||||
StatusMessage,
|
||||
} from 'typings/Queue';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
interface QueueStatusCellProps {
|
||||
sourceTitle: string;
|
||||
status: string;
|
||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState?: QueueTrackedDownloadState;
|
||||
statusMessages?: StatusMessage[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
function QueueStatusCell(props: QueueStatusCellProps) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus = 'ok',
|
||||
trackedDownloadState = 'downloading',
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.status}>
|
||||
<QueueStatus
|
||||
sourceTitle={sourceTitle}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
position="right"
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueStatusCell;
|
||||
@@ -12,7 +12,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
export interface RemovePressProps {
|
||||
remove: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
@@ -21,7 +21,7 @@ interface RemovePressProps {
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
sourceTitle?: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
@@ -39,7 +39,7 @@ type BlocklistMethod =
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
sourceTitle = '',
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
|
||||
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||
import createQueueStatusSelector from './createQueueStatusSelector';
|
||||
|
||||
function QueueStatus() {
|
||||
const dispatch = useDispatch();
|
||||
const { isConnected, isReconnecting } = useSelector(
|
||||
(state: AppState) => state.app
|
||||
);
|
||||
const { isPopulated, count, errors, warnings } = useSelector(
|
||||
createQueueStatusSelector()
|
||||
);
|
||||
|
||||
const wasReconnecting = usePrevious(isReconnecting);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPopulated) {
|
||||
dispatch(fetchQueueStatus());
|
||||
}
|
||||
}, [isPopulated, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && wasReconnecting) {
|
||||
dispatch(fetchQueueStatus());
|
||||
}
|
||||
}, [isConnected, wasReconnecting, dispatch]);
|
||||
|
||||
return (
|
||||
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
|
||||
);
|
||||
}
|
||||
|
||||
export default QueueStatus;
|
||||
@@ -1,76 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app,
|
||||
(state) => state.queue.status,
|
||||
(state) => state.queue.options.includeUnknownMovieItems,
|
||||
(app, status, includeUnknownMovieItems) => {
|
||||
const {
|
||||
errors,
|
||||
warnings,
|
||||
unknownErrors,
|
||||
unknownWarnings,
|
||||
count,
|
||||
totalCount
|
||||
} = status.item;
|
||||
|
||||
return {
|
||||
isConnected: app.isConnected,
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: status.isPopulated,
|
||||
...status.item,
|
||||
count: includeUnknownMovieItems ? totalCount : count,
|
||||
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
||||
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQueueStatus
|
||||
};
|
||||
|
||||
class QueueStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageSidebarStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueStatusConnector.propTypes = {
|
||||
isConnected: PropTypes.bool.isRequired,
|
||||
isReconnecting: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
fetchQueueStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createQueueStatusSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.status.isPopulated,
|
||||
(state: AppState) => state.queue.status.item,
|
||||
(state: AppState) => state.queue.options.includeUnknownMovieItems,
|
||||
(isPopulated, status, includeUnknownMovieItems) => {
|
||||
const {
|
||||
errors,
|
||||
warnings,
|
||||
unknownErrors,
|
||||
unknownWarnings,
|
||||
count,
|
||||
totalCount,
|
||||
} = status;
|
||||
|
||||
return {
|
||||
...status,
|
||||
isPopulated,
|
||||
count: includeUnknownMovieItems ? totalCount : count,
|
||||
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
||||
warnings: includeUnknownMovieItems
|
||||
? warnings || unknownWarnings
|
||||
: warnings,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createQueueStatusSelector;
|
||||
@@ -1,4 +1,3 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
@@ -11,7 +10,18 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TimeleftCell.css';
|
||||
|
||||
function TimeleftCell(props) {
|
||||
interface TimeleftCellProps {
|
||||
estimatedCompletionTime?: string;
|
||||
timeleft?: string;
|
||||
status: string;
|
||||
size: number;
|
||||
sizeleft: number;
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
timeFormat: string;
|
||||
}
|
||||
|
||||
function TimeleftCell(props: TimeleftCellProps) {
|
||||
const {
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
@@ -20,12 +30,18 @@ function TimeleftCell(props) {
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
timeFormat,
|
||||
} = props;
|
||||
|
||||
if (status === 'delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
});
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
@@ -40,8 +56,14 @@ function TimeleftCell(props) {
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
});
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||
includeMinuteZero: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
@@ -56,11 +78,7 @@ function TimeleftCell(props) {
|
||||
}
|
||||
|
||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
|
||||
}
|
||||
|
||||
const totalSize = formatBytes(size);
|
||||
@@ -76,15 +94,4 @@ function TimeleftCell(props) {
|
||||
);
|
||||
}
|
||||
|
||||
TimeleftCell.propTypes = {
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default TimeleftCell;
|
||||
@@ -155,26 +155,27 @@ class AddNewMovieSearchResult extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.icons}>
|
||||
<div>
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title={translate('AlreadyInYourLibrary')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title={translate('AlreadyInYourLibrary')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExcluded &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
size={36}
|
||||
title={translate('MovieIsOnImportExclusionList')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isExcluded &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
size={36}
|
||||
title={translate('MovieIsOnImportExclusionList')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import Queue from 'Activity/Queue/Queue';
|
||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
@@ -115,12 +115,12 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/activity/queue"
|
||||
component={QueueConnector}
|
||||
component={Queue}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/activity/blocklist"
|
||||
component={BlocklistConnector}
|
||||
component={Blocklist}
|
||||
/>
|
||||
|
||||
{/*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Column from 'Components/Table/Column';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
@@ -23,8 +24,13 @@ export interface PagedAppSectionState {
|
||||
totalPages: number;
|
||||
totalRecords?: number;
|
||||
}
|
||||
export interface TableAppSectionState {
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
selectedFilterKey: string;
|
||||
filters: PropertyFilter[];
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ export interface CustomFilter {
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
version: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState,
|
||||
} from './AppSectionState';
|
||||
|
||||
interface BlocklistAppState
|
||||
extends AppSectionState<Blocklist>,
|
||||
AppSectionFilterState<Blocklist> {}
|
||||
AppSectionFilterState<Blocklist>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {
|
||||
isRemoving: boolean;
|
||||
}
|
||||
|
||||
export default BlocklistAppState;
|
||||
|
||||
@@ -20,8 +20,10 @@ export interface MovieIndexAppState {
|
||||
showTitle: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showCinemaRelease: boolean;
|
||||
showDigitalRelease: boolean;
|
||||
showPhysicalRelease: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
|
||||
@@ -3,15 +3,29 @@ import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
AppSectionItemState,
|
||||
Error,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState,
|
||||
} from './AppSectionState';
|
||||
|
||||
export interface QueueStatus {
|
||||
totalCount: number;
|
||||
count: number;
|
||||
unknownCount: number;
|
||||
errors: boolean;
|
||||
warnings: boolean;
|
||||
unknownErrors: boolean;
|
||||
unknownWarnings: boolean;
|
||||
}
|
||||
|
||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface QueuePagedAppState
|
||||
extends AppSectionState<Queue>,
|
||||
AppSectionFilterState<Queue> {
|
||||
AppSectionFilterState<Queue>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {
|
||||
isGrabbing: boolean;
|
||||
grabError: Error;
|
||||
isRemoving: boolean;
|
||||
@@ -19,9 +33,12 @@ export interface QueuePagedAppState
|
||||
}
|
||||
|
||||
interface QueueAppState {
|
||||
status: AppSectionItemState<Queue>;
|
||||
status: AppSectionItemState<QueueStatus>;
|
||||
details: QueueDetailsAppState;
|
||||
paged: QueuePagedAppState;
|
||||
options: {
|
||||
includeUnknownMovieItems: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default QueueAppState;
|
||||
|
||||
@@ -6,6 +6,7 @@ import AppSectionState, {
|
||||
PagedAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||
@@ -14,11 +15,18 @@ import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface GeneralAppState
|
||||
extends AppSectionItemState<General>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListAppState
|
||||
@@ -29,7 +37,9 @@ export interface ImportListAppState
|
||||
export interface IndexerAppState
|
||||
extends AppSectionState<Indexer>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
AppSectionSaveState {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationAppState
|
||||
extends AppSectionState<Notification>,
|
||||
@@ -39,6 +49,11 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListOptionsSettingsAppState
|
||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
@@ -57,7 +72,9 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
customFormats: CustomFormatAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
importLists: ImportListAppState;
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import DiskSpace from 'typings/DiskSpace';
|
||||
import Health from 'typings/Health';
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
import Task from 'typings/Task';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||
export type HealthAppState = AppSectionState<Health>;
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type TaskAppState = AppSectionState<Task>;
|
||||
|
||||
interface SystemAppState {
|
||||
diskSpace: DiskSpaceAppState;
|
||||
health: HealthAppState;
|
||||
status: SystemStatusAppState;
|
||||
tasks: TaskAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
|
||||
@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
|
||||
if (view === calendarViews.WEEK) {
|
||||
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
||||
} else if (view === calendarViews.FORECAST) {
|
||||
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
|
||||
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './CircularProgressBar.css';
|
||||
|
||||
class CircularProgressBar extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
progress: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._progressStep();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const progress = this.props.progress;
|
||||
|
||||
if (prevProps.progress !== progress) {
|
||||
this._cancelProgressStep();
|
||||
this._progressStep();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._cancelProgressStep();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_progressStep() {
|
||||
this.requestAnimationFrame = window.requestAnimationFrame(() => {
|
||||
this.setState({
|
||||
progress: this.state.progress + 1
|
||||
}, () => {
|
||||
if (this.state.progress < this.props.progress) {
|
||||
this._progressStep();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_cancelProgressStep() {
|
||||
if (this.requestAnimationFrame) {
|
||||
window.cancelAnimationFrame(this.requestAnimationFrame);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
containerClassName,
|
||||
size,
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
showProgressText
|
||||
} = this.props;
|
||||
|
||||
const progress = this.state.progress;
|
||||
|
||||
const center = size / 2;
|
||||
const radius = center - strokeWidth;
|
||||
const circumference = Math.PI * (radius * 2);
|
||||
const sizeInPixels = `${size}px`;
|
||||
const strokeDashoffset = ((100 - progress) / 100) * circumference;
|
||||
const progressText = `${Math.round(progress)}%`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClassName}
|
||||
style={{
|
||||
width: sizeInPixels,
|
||||
height: sizeInPixels,
|
||||
lineHeight: sizeInPixels
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
>
|
||||
<circle
|
||||
fill="transparent"
|
||||
r={radius}
|
||||
cx={center}
|
||||
cy={center}
|
||||
strokeDasharray={circumference}
|
||||
style={{
|
||||
stroke: strokeColor,
|
||||
strokeWidth,
|
||||
strokeDashoffset
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{
|
||||
showProgressText &&
|
||||
<div className={styles.circularProgressBarText}>
|
||||
{progressText}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgressBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
containerClassName: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
progress: PropTypes.number.isRequired,
|
||||
strokeWidth: PropTypes.number,
|
||||
strokeColor: PropTypes.string,
|
||||
showProgressText: PropTypes.bool
|
||||
};
|
||||
|
||||
CircularProgressBar.defaultProps = {
|
||||
className: styles.circularProgressBar,
|
||||
containerClassName: styles.circularProgressBarContainer,
|
||||
size: 60,
|
||||
strokeWidth: 5,
|
||||
strokeColor: '#ffc230',
|
||||
showProgressText: false
|
||||
};
|
||||
|
||||
export default CircularProgressBar;
|
||||
99
frontend/src/Components/CircularProgressBar.tsx
Normal file
99
frontend/src/Components/CircularProgressBar.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styles from './CircularProgressBar.css';
|
||||
|
||||
interface CircularProgressBarProps {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
size?: number;
|
||||
progress: number;
|
||||
strokeWidth?: number;
|
||||
strokeColor?: string;
|
||||
showProgressText?: boolean;
|
||||
}
|
||||
|
||||
function CircularProgressBar({
|
||||
className = styles.circularProgressBar,
|
||||
containerClassName = styles.circularProgressBarContainer,
|
||||
size = 60,
|
||||
strokeWidth = 5,
|
||||
strokeColor = '#ffc230',
|
||||
showProgressText = false,
|
||||
progress,
|
||||
}: CircularProgressBarProps) {
|
||||
const [currentProgress, setCurrentProgress] = useState(0);
|
||||
const raf = React.useRef<number>(0);
|
||||
const center = size / 2;
|
||||
const radius = center - strokeWidth;
|
||||
const circumference = Math.PI * (radius * 2);
|
||||
const sizeInPixels = `${size}px`;
|
||||
const strokeDashoffset = ((100 - currentProgress) / 100) * circumference;
|
||||
const progressText = `${Math.round(currentProgress)}%`;
|
||||
|
||||
const handleAnimation = useCallback(
|
||||
(p: number) => {
|
||||
setCurrentProgress((prevProgress) => {
|
||||
if (prevProgress < p) {
|
||||
return prevProgress + Math.min(1, p - prevProgress);
|
||||
}
|
||||
|
||||
return prevProgress;
|
||||
});
|
||||
},
|
||||
[setCurrentProgress]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress > currentProgress) {
|
||||
cancelAnimationFrame(raf.current);
|
||||
|
||||
raf.current = requestAnimationFrame(() => handleAnimation(progress));
|
||||
}
|
||||
}, [progress, currentProgress, handleAnimation]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
return () => cancelAnimationFrame(raf.current);
|
||||
},
|
||||
// We only want to run this effect once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClassName}
|
||||
style={{
|
||||
width: sizeInPixels,
|
||||
height: sizeInPixels,
|
||||
lineHeight: sizeInPixels,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
>
|
||||
<circle
|
||||
fill="transparent"
|
||||
r={radius}
|
||||
cx={center}
|
||||
cy={center}
|
||||
strokeDasharray={circumference}
|
||||
style={{
|
||||
stroke: strokeColor,
|
||||
strokeWidth,
|
||||
strokeDashoffset,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{showProgressText && (
|
||||
<div className={styles.circularProgressBarText}>{progressText}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CircularProgressBar;
|
||||
@@ -49,7 +49,11 @@ class TextTagInputConnector extends Component {
|
||||
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
|
||||
|
||||
newTags.forEach((newTag) => {
|
||||
newValue.push(newTag.trim());
|
||||
const newTagValue = newTag.trim();
|
||||
|
||||
if (newTagValue) {
|
||||
newValue.push(newTagValue);
|
||||
}
|
||||
});
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
@@ -80,7 +84,12 @@ class TextTagInputConnector extends Component {
|
||||
|
||||
const newValue = [...valueArray];
|
||||
newValue.splice(tagToReplace.index, 1);
|
||||
newValue.push(newTag.name.trim());
|
||||
|
||||
const newTagValue = newTag.name.trim();
|
||||
|
||||
if (newTagValue) {
|
||||
newValue.push(newTagValue);
|
||||
}
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
};
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './Icon.css';
|
||||
|
||||
class Icon extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
containerClassName,
|
||||
className,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
title,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const icon = (
|
||||
<FontAwesomeIcon
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind]
|
||||
)}
|
||||
icon={name}
|
||||
spin={isSpinning}
|
||||
style={{
|
||||
fontSize: `${size}px`
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
|
||||
if (title) {
|
||||
return (
|
||||
<span
|
||||
className={containerClassName}
|
||||
title={typeof title === 'function' ? title() : title}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
Icon.propTypes = {
|
||||
containerClassName: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
fixedWidth: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
Icon.defaultProps = {
|
||||
kind: kinds.DEFAULT,
|
||||
size: 14,
|
||||
isSpinning: false,
|
||||
fixedWidth: false
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
60
frontend/src/Components/Icon.tsx
Normal file
60
frontend/src/Components/Icon.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconProps,
|
||||
} from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import styles from './Icon.css';
|
||||
|
||||
export interface IconProps
|
||||
extends Omit<
|
||||
FontAwesomeIconProps,
|
||||
'icon' | 'spin' | 'name' | 'title' | 'size'
|
||||
> {
|
||||
containerClassName?: ComponentProps<'span'>['className'];
|
||||
name: FontAwesomeIconProps['icon'];
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: number;
|
||||
isSpinning?: FontAwesomeIconProps['spin'];
|
||||
title?: string | (() => string);
|
||||
}
|
||||
|
||||
export default function Icon({
|
||||
containerClassName,
|
||||
className,
|
||||
name,
|
||||
kind = kinds.DEFAULT,
|
||||
size = 14,
|
||||
title,
|
||||
isSpinning = false,
|
||||
fixedWidth = false,
|
||||
...otherProps
|
||||
}: IconProps) {
|
||||
const icon = (
|
||||
<FontAwesomeIcon
|
||||
className={classNames(className, styles[kind])}
|
||||
icon={name}
|
||||
spin={isSpinning}
|
||||
fixedWidth={fixedWidth}
|
||||
style={{
|
||||
fontSize: `${size}px`,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
|
||||
if (title) {
|
||||
return (
|
||||
<span
|
||||
className={containerClassName}
|
||||
title={typeof title === 'function' ? title() : title}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
47
frontend/src/Components/ImdbRating.tsx
Normal file
47
frontend/src/Components/ImdbRating.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -1,52 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './Label.css';
|
||||
|
||||
function Label(props) {
|
||||
const {
|
||||
className,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
children,
|
||||
colorImpairedMode,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline,
|
||||
colorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
colorImpairedMode: PropTypes.bool
|
||||
};
|
||||
|
||||
Label.defaultProps = {
|
||||
className: styles.label,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.SMALL,
|
||||
outline: false,
|
||||
colorImpairedMode: false
|
||||
};
|
||||
|
||||
export default Label;
|
||||
33
frontend/src/Components/Label.tsx
Normal file
33
frontend/src/Components/Label.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import styles from './Label.css';
|
||||
|
||||
export interface LabelProps extends ComponentProps<'span'> {
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
outline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Label({
|
||||
className = styles.label,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.SMALL,
|
||||
outline = false,
|
||||
...otherProps
|
||||
}: LabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import Link from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
class Button extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
buttonGroupPosition,
|
||||
kind,
|
||||
size,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
buttonGroupPosition: PropTypes.oneOf(align.all),
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
size: PropTypes.oneOf(sizes.all),
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
className: styles.button,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.MEDIUM
|
||||
};
|
||||
|
||||
export default Button;
|
||||
37
frontend/src/Components/Link/Button.tsx
Normal file
37
frontend/src/Components/Link/Button.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import Link, { LinkProps } from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
||||
buttonGroupPosition?: Extract<
|
||||
(typeof align.all)[number],
|
||||
keyof typeof styles
|
||||
>;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
children: Required<LinkProps['children']>;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
className = styles.button,
|
||||
buttonGroupPosition,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.MEDIUM,
|
||||
...otherProps
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Link from './Link';
|
||||
import styles from './IconButton.css';
|
||||
|
||||
function IconButton(props) {
|
||||
const {
|
||||
className,
|
||||
iconClassName,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
isSpinning,
|
||||
isDisabled,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
aria-label={translate('TableOptionsButton')}
|
||||
isDisabled={isDisabled}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
className={iconClassName}
|
||||
name={name}
|
||||
kind={kind}
|
||||
size={size}
|
||||
isSpinning={isSpinning}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
IconButton.propTypes = {
|
||||
...Link.propTypes,
|
||||
className: PropTypes.string.isRequired,
|
||||
iconClassName: PropTypes.string,
|
||||
kind: PropTypes.string,
|
||||
name: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
isSpinning: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool
|
||||
};
|
||||
|
||||
IconButton.defaultProps = {
|
||||
className: styles.button,
|
||||
size: 12
|
||||
};
|
||||
|
||||
export default IconButton;
|
||||
41
frontend/src/Components/Link/IconButton.tsx
Normal file
41
frontend/src/Components/Link/IconButton.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from 'Components/Icon';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Link, { LinkProps } from './Link';
|
||||
import styles from './IconButton.css';
|
||||
|
||||
export interface IconButtonProps
|
||||
extends Omit<LinkProps, 'name' | 'kind'>,
|
||||
Pick<IconProps, 'name' | 'kind' | 'size' | 'isSpinning'> {
|
||||
iconClassName?: IconProps['className'];
|
||||
}
|
||||
|
||||
export default function IconButton({
|
||||
className = styles.button,
|
||||
iconClassName,
|
||||
name,
|
||||
kind,
|
||||
size = 12,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
}: IconButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
otherProps.isDisabled && styles.isDisabled
|
||||
)}
|
||||
aria-label={translate('TableOptionsButton')}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
className={iconClassName}
|
||||
name={name}
|
||||
kind={kind}
|
||||
size={size}
|
||||
isSpinning={isSpinning}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +1,93 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ComponentClass,
|
||||
FunctionComponent,
|
||||
ComponentPropsWithoutRef,
|
||||
ElementType,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
interface ReactRouterLinkProps {
|
||||
to?: string;
|
||||
}
|
||||
export type LinkProps<C extends ElementType = 'button'> =
|
||||
ComponentPropsWithoutRef<C> & {
|
||||
component?: C;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: LinkProps<C>['disabled'];
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
};
|
||||
|
||||
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
||||
className?: string;
|
||||
component?:
|
||||
| string
|
||||
| FunctionComponent<LinkProps>
|
||||
| ComponentClass<LinkProps, unknown>;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: boolean;
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
}
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
className,
|
||||
component = 'button',
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter = false,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
export default function Link<C extends ElementType = 'button'>({
|
||||
className,
|
||||
component,
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: LinkProps<C>) {
|
||||
const Component = component || 'button';
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onPress?.(event);
|
||||
},
|
||||
[isDisabled, onPress]
|
||||
);
|
||||
|
||||
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
||||
target,
|
||||
};
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if (/\w+?:\/\//.test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Radarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
const linkClass = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const elementProps = {
|
||||
...otherProps,
|
||||
type,
|
||||
...linkProps,
|
||||
};
|
||||
if (to) {
|
||||
const toLink = /\w+?:\/\//.test(to);
|
||||
|
||||
elementProps.onClick = onClick;
|
||||
if (toLink || noRouter) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
target={target || (toLink ? '_blank' : '_self')}
|
||||
rel={toLink ? 'noreferrer' : undefined}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement(el, elementProps);
|
||||
return (
|
||||
<RouterLink
|
||||
to={`${window.Radarr.urlBase}/${to.replace(/^\//, '')}`}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
type={
|
||||
component === 'button' || component === 'input'
|
||||
? type || 'button'
|
||||
: type
|
||||
}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Link;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
&.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import MovieSearchInputConnector from './MovieSearchInputConnector';
|
||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
class PageHeader extends Component {
|
||||
@@ -84,6 +84,7 @@ class PageHeader extends Component {
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.translate}
|
||||
title={translate('SuggestTranslationChange')}
|
||||
@@ -91,7 +92,8 @@ class PageHeader extends Component {
|
||||
to="https://translate.servarr.com/projects/radarr/radarr/"
|
||||
size={24}
|
||||
/>
|
||||
<PageHeaderActionsMenuConnector
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
function PageHeaderActionsMenu(props) {
|
||||
const {
|
||||
formsAuth,
|
||||
onKeyboardShortcutsPress,
|
||||
onRestartPress,
|
||||
onShutdownPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.KEYBOARD}
|
||||
/>
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={onRestartPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.RESTART}
|
||||
/>
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={onShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<div className={styles.separator} />
|
||||
}
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<MenuItem
|
||||
to={`${window.Radarr.urlBase}/logout`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.LOGOUT}
|
||||
/>
|
||||
Logout
|
||||
</MenuItem>
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PageHeaderActionsMenu.propTypes = {
|
||||
formsAuth: PropTypes.bool.isRequired,
|
||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
||||
onRestartPress: PropTypes.func.isRequired,
|
||||
onShutdownPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
interface PageHeaderActionsMenuProps {
|
||||
onKeyboardShortcutsPress(): void;
|
||||
}
|
||||
|
||||
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||
const { onKeyboardShortcutsPress } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { authentication, isDocker } = useSelector(
|
||||
(state: AppState) => state.system.status.item
|
||||
);
|
||||
|
||||
const formsAuth = authentication === 'forms';
|
||||
|
||||
const handleRestartPress = useCallback(() => {
|
||||
dispatch(restart());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleShutdownPress = useCallback(() => {
|
||||
dispatch(shutdown());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
{isDocker ? null : (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={handleRestartPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={handleShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{formsAuth ? (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem to={`${window.Radarr.urlBase}/logout`} noRouter={true}>
|
||||
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
||||
{translate('Logout')}
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -1,56 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
(status) => {
|
||||
return {
|
||||
formsAuth: status.item.authentication === 'forms'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
restart,
|
||||
shutdown
|
||||
};
|
||||
|
||||
class PageHeaderActionsMenuConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRestartPress = () => {
|
||||
this.props.restart();
|
||||
};
|
||||
|
||||
onShutdownPress = () => {
|
||||
this.props.shutdown();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageHeaderActionsMenu
|
||||
{...this.props}
|
||||
onRestartPress={this.onRestartPress}
|
||||
onShutdownPress={this.onShutdownPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageHeaderActionsMenuConnector.propTypes = {
|
||||
restart: PropTypes.func.isRequired,
|
||||
shutdown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
||||
@@ -3,13 +3,13 @@ import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import QueueStatusConnector from 'Activity/Queue/Status/QueueStatusConnector';
|
||||
import QueueStatus from 'Activity/Queue/Status/QueueStatus';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
|
||||
import HealthStatus from 'System/Status/Health/HealthStatus';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MessagesConnector from './Messages/MessagesConnector';
|
||||
import PageSidebarItem from './PageSidebarItem';
|
||||
@@ -58,7 +58,7 @@ const links = [
|
||||
{
|
||||
title: () => translate('Queue'),
|
||||
to: '/activity/queue',
|
||||
statusComponent: QueueStatusConnector
|
||||
statusComponent: QueueStatus
|
||||
},
|
||||
{
|
||||
title: () => translate('History'),
|
||||
@@ -151,7 +151,7 @@ const links = [
|
||||
{
|
||||
title: () => translate('Status'),
|
||||
to: '/system/status',
|
||||
statusComponent: HealthStatusConnector
|
||||
statusComponent: HealthStatus
|
||||
},
|
||||
{
|
||||
title: () => translate('Tasks'),
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ProgressBar.css';
|
||||
|
||||
function ProgressBar(props) {
|
||||
const {
|
||||
className,
|
||||
containerClassName,
|
||||
title,
|
||||
progress,
|
||||
precision,
|
||||
showText,
|
||||
text,
|
||||
kind,
|
||||
size,
|
||||
width
|
||||
} = props;
|
||||
|
||||
const progressPercent = `${progress.toFixed(precision)}%`;
|
||||
const progressText = text || progressPercent;
|
||||
const actualWidth = width ? `${width}px` : '100%';
|
||||
|
||||
return (
|
||||
<ColorImpairedConsumer>
|
||||
{(enableColorImpairedMode) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
styles[size]
|
||||
)}
|
||||
title={title}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
{
|
||||
showText && width ?
|
||||
<div
|
||||
className={classNames(styles.backTextContainer, styles[kind])}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div className={styles.backText}>
|
||||
<div>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
role="meter"
|
||||
aria-label={translate('ProgressBarProgress', { progress: progress.toFixed(0) })}
|
||||
aria-valuenow={progress.toFixed(0)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={{ width: progressPercent }}
|
||||
/>
|
||||
|
||||
{
|
||||
showText ?
|
||||
<div
|
||||
className={classNames(styles.frontTextContainer, styles[kind])}
|
||||
style={{ width: progressPercent }}
|
||||
>
|
||||
<div
|
||||
className={styles.frontText}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ColorImpairedConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
ProgressBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
containerClassName: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
progress: PropTypes.number.isRequired,
|
||||
precision: PropTypes.number.isRequired,
|
||||
showText: PropTypes.bool.isRequired,
|
||||
text: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
width: PropTypes.number
|
||||
};
|
||||
|
||||
ProgressBar.defaultProps = {
|
||||
className: styles.progressBar,
|
||||
containerClassName: styles.container,
|
||||
precision: 1,
|
||||
showText: false,
|
||||
kind: kinds.PRIMARY,
|
||||
size: sizes.MEDIUM
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
||||
94
frontend/src/Components/ProgressBar.tsx
Normal file
94
frontend/src/Components/ProgressBar.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ProgressBar.css';
|
||||
|
||||
interface ProgressBarProps {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
title?: string;
|
||||
progress: number;
|
||||
precision?: number;
|
||||
showText?: boolean;
|
||||
text?: string;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function ProgressBar({
|
||||
className = styles.progressBar,
|
||||
containerClassName = styles.container,
|
||||
title,
|
||||
progress,
|
||||
precision = 1,
|
||||
showText = false,
|
||||
text,
|
||||
kind = 'primary',
|
||||
size = 'medium',
|
||||
width,
|
||||
}: ProgressBarProps) {
|
||||
const progressPercent = `${progress.toFixed(precision)}%`;
|
||||
const progressText = text || progressPercent;
|
||||
const actualWidth = width ? `${width}px` : '100%';
|
||||
|
||||
return (
|
||||
<ColorImpairedConsumer>
|
||||
{(enableColorImpairedMode) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(containerClassName, styles[size])}
|
||||
title={title}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
{showText && width ? (
|
||||
<div
|
||||
className={classNames(styles.backTextContainer, styles[kind])}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div className={styles.backText}>
|
||||
<div>{progressText}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
role="meter"
|
||||
aria-label={translate('ProgressBarProgress', {
|
||||
progress: progress.toFixed(0),
|
||||
})}
|
||||
aria-valuenow={Math.floor(progress)}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
style={{ width: progressPercent }}
|
||||
/>
|
||||
|
||||
{showText ? (
|
||||
<div
|
||||
className={classNames(styles.frontTextContainer, styles[kind])}
|
||||
style={{ width: progressPercent }}
|
||||
>
|
||||
<div
|
||||
className={styles.frontText}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div>{progressText}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ColorImpairedConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProgressBar;
|
||||
@@ -1,60 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import styles from './RottenTomatoRating.css';
|
||||
|
||||
class RottenTomatoRating extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
ratings,
|
||||
hideIcon,
|
||||
iconSize
|
||||
} = this.props;
|
||||
|
||||
const rtRotten = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNDQ1LjE4NSA0NDQuNjg0Yy03OS4zNjkgNC4xNjctOTUuNTg3LTg2LjY1Mi0xMjYuNzI2LTg2LjAwNi0xMy4yNjguMjc5LTIzLjcyNiAxNC4xNTEtMTkuMTMzIDMwLjMyIDIuNTI1IDguODg4IDkuNTMgMjEuOTIzIDEzLjk0NCAzMC4wMTEgMTUuNTcgMjguNTQ0LTcuNDQ3IDYwLjg0NS0zNC4zODMgNjMuNTc3LTQ0Ljc2IDQuNTQtNjMuNDMzLTIxLjQyNi02Mi4yNzgtNDguMDA3IDEuMy0yOS44NCAyNi42LTYwLjMzMS42NS03My4zMDUtMjcuMTk0LTEzLjU5Ny00OS4zMDEgMzkuNTcyLTc1LjMyNSA1MS40MzktMjMuNTUzIDEwLjc0MS01Ni4yNDggMi40MTMtNjcuODcyLTIzLjc0MS04LjE2NC0xOC4zNzktNi42OC01My43NjggMjkuNjctNjcuMjcgMjIuNzA2LTguNDMzIDczLjMwNSAxMS4wMjkgNzUuOS0xMy42MjMgMi45OTItMjguNDE2LTUzLjE1NS0zMC44MTItNzAuMDYtMzcuNjI2LTI5LjkxMi0xMi4wNTUtNDcuNTY3LTM3Ljg1LTMzLjczNC02NS41MjIgMTAuMzc4LTIwLjc1NyA0MC45MTUtMjkuMjAzIDY0LjIyMy0yMC4xMSAyNy45MjIgMTAuODkyIDMyLjQwNCAzOS44NTMgNDYuNzEgNTEuODk3IDEyLjMyNCAxMC4zOCAyOS4xOSAxMS42OCA0MC4yMiA0LjU0MyA4LjEzNS01LjI2NSAxMC44NDMtMTYuODI4IDcuNzc0LTI3LjM5LTQuMDctMTQuMDIzLTE0Ljg3NS0yMi43NzMtMjUuNDE1LTMxLjM0Ni0xOC43NTgtMTUuMjQ5LTQ1LjI0LTI4LjM2LTI5LjIyMi02OS45ODMgMTMuMTMtMzQuMTEgNTEuNjQyLTM1LjM0IDUxLjY0Mi0zNS4zNCAxNS4zLTEuNzIgMjkuMDAyIDIuOSA0MC4xNjcgMTIuODc1IDE0LjkyNyAxMy4zMzUgMTcuODM0IDMxLjE2IDE1LjMzNiA1MC4xNzYtMi4yODMgMTcuMzU4LTguNDI2IDMyLjU2LTExLjYzIDQ5Ljc1OS0zLjcxNyAxOS45NjYgNi45NTQgNDAuMDg2IDI3LjI0OSA0MC44NjkgMjYuNjk0IDEuMDMxIDM0LjY5OC0xOS40ODYgMzcuOTY0LTMyLjQ5MiA0Ljc4Mi0xOS4wMjggMTEuMDU4LTM2LjY5NCAyOC43MTgtNDcuODIgMjUuMzQ2LTE1Ljk3IDYwLjU1Mi0xMi40NyA3Ni44ODYgMTguMjIyIDEyLjkyIDI0LjI4NCA4Ljc3MiA1Ny43MTUtMTEuMDQ3IDc1Ljk3LTguODkyIDguMTg4LTE5LjU4NCAxMS4wNzUtMzEuMTQ4IDExLjE1Ni0xNi41ODUuMTE3LTMzLjE2Mi0uMjktNDguNTU2IDcuNDcxLTEwLjQ4IDUuMjgxLTE1LjA0NyAxMy44ODgtMTUuMDQ1IDI1LjQyMyAwIDExLjI0MiA1Ljg1MyAxOC41ODUgMTUuMzM2IDIzLjM2MyAxNy44NiA5LjAwMyAzNy41NzcgMTAuODQzIDU2Ljg3MSAxNC4yMjIgMjcuOTggNC45IDUyLjU4MSAxNC43NTUgNjguMzc1IDQwLjcyLjE0Mi4yMjguMjguNDU4LjQxNS42OSAxOC4xMzkgMzAuNzQxLS44MzEgNzUuMDA1LTM2LjQ3NiA3Ni44NzgiIGZpbGw9IiMwQUM4NTUiLz48L3N2Zz4=';
|
||||
const rtFresh = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNDc4LjI5IDI5Ni45OGMtMy45OS02My45NjYtMzYuNTItMTExLjgyLTg1LjQ2OC0xMzguNTggMC4yNzggMS41Ni0xLjEwOSAzLjUwOC0yLjY4OCAyLjgxOC0zMi4wMTYtMTQuMDA2LTg2LjMyOCAzMS4zMi0xMjQuMjggNy41ODQgMC4yODUgOC41MTktMS4zNzggNTAuMDcyLTU5LjkxNCA1Mi40ODMtMS4zODIgMC4wNTYtMi4xNDItMS4zNTUtMS4yNjgtMi4zNTQgNy44MjgtOC45MjkgMTUuNzMyLTMxLjUzNSA0LjM2Ny00My41ODYtMjQuMzM4IDIxLjgxLTM4LjQ3MiAzMC4wMTctODUuMTM4IDE5LjE4Ni0yOS44NzggMzEuMjQxLTQ2LjgwOSA3NC00My40ODUgMTI3LjI2IDYuNzggMTA4Ljc0IDEwOC42MyAxNzAuODkgMjExLjE5IDE2NC40OSAxMDIuNTYtNi4zOTUgMTkzLjQ3LTgwLjU3MiAxODYuNjgtMTg5LjMxIiBmaWxsPSIjRkEzMjBBIi8+PHBhdGggZD0iTTI5MS4zNzUgMTMyLjI5M2MyMS4wNzUtNS4wMjMgODEuNjkzLS40OSAxMDEuMTE0IDI1LjI3NCAxLjE2NiAxLjU0NS0uNDc1IDQuNDY4LTIuMzU1IDMuNjQ4LTMyLjAxNi0xNC4wMDYtODYuMzI4IDMxLjMyLTEyNC4yODIgNy41ODQuMjg1IDguNTE5LTEuMzc4IDUwLjA3Mi01OS45MTQgNTIuNDgzLTEuMzgyLjA1Ni0yLjE0Mi0xLjM1NS0xLjI2OC0yLjM1NCA3LjgyOC04LjkyOSAxNS43My0zMS41MzUgNC4zNjctNDMuNTg2LTI2LjUxMiAyMy43NTgtNDAuODg0IDMxLjM5Mi05OC40MjYgMTUuODM4LTEuODgzLS41MDgtMS4yNDEtMy41MzUuNzYyLTQuMjk4IDEwLjg3Ni00LjE1NyAzNS41MTUtMjIuMzYxIDU4LjgyNC0zMC4zODUgNC40MzgtMS41MjYgOC44NjItMi43MSAxMy4xOC0zLjQtMjUuNjY1LTIuMjkzLTM3LjIzNS01Ljg2Mi01My41NTktMy40LTEuNzg5LjI3LTMuMDA0LTEuODEzLTEuODk1LTMuMjQxIDIxLjk5NS0yOC4zMzIgNjIuNTEzLTM2Ljg4OCA4Ny41MTItMjEuODM3LTE1LjQxLTE5LjA5NC0yNy40OC0zNC4zMjEtMjcuNDgtMzQuMzIxbDI4LjYwMS0xNi4yNDZzMTEuODE3IDI2LjQgMjAuNDE0IDQ1LjYxNGMyMS4yNzUtMzEuNDM1IDYwLjg2LTM0LjMzNiA3Ny41ODUtMTIuMDMzLjk5MiAxLjMyNi0uMDQ1IDMuMjEtMS43MDIgMy4xNzEtMTMuNjEyLS4zMzEtMjEuMTA3IDEyLjA1LTIxLjY3NSAyMS40NjZsLjE5Ny4wMjMiIGZpbGw9IiMwMDkxMkQiLz48L3N2Zz4=';
|
||||
|
||||
const rating = ratings.rottenTomatoes;
|
||||
|
||||
let ratingString = '0%';
|
||||
let ratingImage = rtFresh;
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value}%`;
|
||||
ratingImage = rating.value > 50 ? rtFresh : rtRotten;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
!hideIcon &&
|
||||
<img
|
||||
className={styles.image}
|
||||
src={ratingImage}
|
||||
style={{
|
||||
height: `${iconSize}px`
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{ratingString}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RottenTomatoRating.propTypes = {
|
||||
ratings: PropTypes.object.isRequired,
|
||||
iconSize: PropTypes.number.isRequired,
|
||||
hideIcon: PropTypes.bool
|
||||
};
|
||||
|
||||
RottenTomatoRating.defaultProps = {
|
||||
iconSize: 14
|
||||
};
|
||||
|
||||
export default RottenTomatoRating;
|
||||
43
frontend/src/Components/RottenTomatoRating.tsx
Normal file
43
frontend/src/Components/RottenTomatoRating.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Ratings, RatingValues } from 'Movie/Movie';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RottenTomatoRating.css';
|
||||
|
||||
interface RottenTomatoRatingProps {
|
||||
ratings: Ratings;
|
||||
iconSize?: number;
|
||||
hideIcon?: boolean;
|
||||
}
|
||||
|
||||
function RottenTomatoRating(props: RottenTomatoRatingProps) {
|
||||
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||
|
||||
const rtRotten =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNDQ1LjE4NSA0NDQuNjg0Yy03OS4zNjkgNC4xNjctOTUuNTg3LTg2LjY1Mi0xMjYuNzI2LTg2LjAwNi0xMy4yNjguMjc5LTIzLjcyNiAxNC4xNTEtMTkuMTMzIDMwLjMyIDIuNTI1IDguODg4IDkuNTMgMjEuOTIzIDEzLjk0NCAzMC4wMTEgMTUuNTcgMjguNTQ0LTcuNDQ3IDYwLjg0NS0zNC4zODMgNjMuNTc3LTQ0Ljc2IDQuNTQtNjMuNDMzLTIxLjQyNi02Mi4yNzgtNDguMDA3IDEuMy0yOS44NCAyNi42LTYwLjMzMS42NS03My4zMDUtMjcuMTk0LTEzLjU5Ny00OS4zMDEgMzkuNTcyLTc1LjMyNSA1MS40MzktMjMuNTUzIDEwLjc0MS01Ni4yNDggMi40MTMtNjcuODcyLTIzLjc0MS04LjE2NC0xOC4zNzktNi42OC01My43NjggMjkuNjctNjcuMjcgMjIuNzA2LTguNDMzIDczLjMwNSAxMS4wMjkgNzUuOS0xMy42MjMgMi45OTItMjguNDE2LTUzLjE1NS0zMC44MTItNzAuMDYtMzcuNjI2LTI5LjkxMi0xMi4wNTUtNDcuNTY3LTM3Ljg1LTMzLjczNC02NS41MjIgMTAuMzc4LTIwLjc1NyA0MC45MTUtMjkuMjAzIDY0LjIyMy0yMC4xMSAyNy45MjIgMTAuODkyIDMyLjQwNCAzOS44NTMgNDYuNzEgNTEuODk3IDEyLjMyNCAxMC4zOCAyOS4xOSAxMS42OCA0MC4yMiA0LjU0MyA4LjEzNS01LjI2NSAxMC44NDMtMTYuODI4IDcuNzc0LTI3LjM5LTQuMDctMTQuMDIzLTE0Ljg3NS0yMi43NzMtMjUuNDE1LTMxLjM0Ni0xOC43NTgtMTUuMjQ5LTQ1LjI0LTI4LjM2LTI5LjIyMi02OS45ODMgMTMuMTMtMzQuMTEgNTEuNjQyLTM1LjM0IDUxLjY0Mi0zNS4zNCAxNS4zLTEuNzIgMjkuMDAyIDIuOSA0MC4xNjcgMTIuODc1IDE0LjkyNyAxMy4zMzUgMTcuODM0IDMxLjE2IDE1LjMzNiA1MC4xNzYtMi4yODMgMTcuMzU4LTguNDI2IDMyLjU2LTExLjYzIDQ5Ljc1OS0zLjcxNyAxOS45NjYgNi45NTQgNDAuMDg2IDI3LjI0OSA0MC44NjkgMjYuNjk0IDEuMDMxIDM0LjY5OC0xOS40ODYgMzcuOTY0LTMyLjQ5MiA0Ljc4Mi0xOS4wMjggMTEuMDU4LTM2LjY5NCAyOC43MTgtNDcuODIgMjUuMzQ2LTE1Ljk3IDYwLjU1Mi0xMi40NyA3Ni44ODYgMTguMjIyIDEyLjkyIDI0LjI4NCA4Ljc3MiA1Ny43MTUtMTEuMDQ3IDc1Ljk3LTguODkyIDguMTg4LTE5LjU4NCAxMS4wNzUtMzEuMTQ4IDExLjE1Ni0xNi41ODUuMTE3LTMzLjE2Mi0uMjktNDguNTU2IDcuNDcxLTEwLjQ4IDUuMjgxLTE1LjA0NyAxMy44ODgtMTUuMDQ1IDI1LjQyMyAwIDExLjI0MiA1Ljg1MyAxOC41ODUgMTUuMzM2IDIzLjM2MyAxNy44NiA5LjAwMyAzNy41NzcgMTAuODQzIDU2Ljg3MSAxNC4yMjIgMjcuOTggNC45IDUyLjU4MSAxNC43NTUgNjguMzc1IDQwLjcyLjE0Mi4yMjguMjguNDU4LjQxNS42OSAxOC4xMzkgMzAuNzQxLS44MzEgNzUuMDA1LTM2LjQ3NiA3Ni44NzgiIGZpbGw9IiMwQUM4NTUiLz48L3N2Zz4=';
|
||||
const rtFresh =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNDc4LjI5IDI5Ni45OGMtMy45OS02My45NjYtMzYuNTItMTExLjgyLTg1LjQ2OC0xMzguNTggMC4yNzggMS41Ni0xLjEwOSAzLjUwOC0yLjY4OCAyLjgxOC0zMi4wMTYtMTQuMDA2LTg2LjMyOCAzMS4zMi0xMjQuMjggNy41ODQgMC4yODUgOC41MTktMS4zNzggNTAuMDcyLTU5LjkxNCA1Mi40ODMtMS4zODIgMC4wNTYtMi4xNDItMS4zNTUtMS4yNjgtMi4zNTQgNy44MjgtOC45MjkgMTUuNzMyLTMxLjUzNSA0LjM2Ny00My41ODYtMjQuMzM4IDIxLjgxLTM4LjQ3MiAzMC4wMTctODUuMTM4IDE5LjE4Ni0yOS44NzggMzEuMjQxLTQ2LjgwOSA3NC00My40ODUgMTI3LjI2IDYuNzggMTA4Ljc0IDEwOC42MyAxNzAuODkgMjExLjE5IDE2NC40OSAxMDIuNTYtNi4zOTUgMTkzLjQ3LTgwLjU3MiAxODYuNjgtMTg5LjMxIiBmaWxsPSIjRkEzMjBBIi8+PHBhdGggZD0iTTI5MS4zNzUgMTMyLjI5M2MyMS4wNzUtNS4wMjMgODEuNjkzLS40OSAxMDEuMTE0IDI1LjI3NCAxLjE2NiAxLjU0NS0uNDc1IDQuNDY4LTIuMzU1IDMuNjQ4LTMyLjAxNi0xNC4wMDYtODYuMzI4IDMxLjMyLTEyNC4yODIgNy41ODQuMjg1IDguNTE5LTEuMzc4IDUwLjA3Mi01OS45MTQgNTIuNDgzLTEuMzgyLjA1Ni0yLjE0Mi0xLjM1NS0xLjI2OC0yLjM1NCA3LjgyOC04LjkyOSAxNS43My0zMS41MzUgNC4zNjctNDMuNTg2LTI2LjUxMiAyMy43NTgtNDAuODg0IDMxLjM5Mi05OC40MjYgMTUuODM4LTEuODgzLS41MDgtMS4yNDEtMy41MzUuNzYyLTQuMjk4IDEwLjg3Ni00LjE1NyAzNS41MTUtMjIuMzYxIDU4LjgyNC0zMC4zODUgNC40MzgtMS41MjYgOC44NjItMi43MSAxMy4xOC0zLjQtMjUuNjY1LTIuMjkzLTM3LjIzNS01Ljg2Mi01My41NTktMy40LTEuNzg5LjI3LTMuMDA0LTEuODEzLTEuODk1LTMuMjQxIDIxLjk5NS0yOC4zMzIgNjIuNTEzLTM2Ljg4OCA4Ny41MTItMjEuODM3LTE1LjQxLTE5LjA5NC0yNy40OC0zNC4zMjEtMjcuNDgtMzQuMzIxbDI4LjYwMS0xNi4yNDZzMTEuODE3IDI2LjQgMjAuNDE0IDQ1LjYxNGMyMS4yNzUtMzEuNDM1IDYwLjg2LTM0LjMzNiA3Ny41ODUtMTIuMDMzLjk5MiAxLjMyNi0uMDQ1IDMuMjEtMS43MDIgMy4xNzEtMTMuNjEyLS4zMzEtMjEuMTA3IDEyLjA1LTIxLjY3NSAyMS40NjZsLjE5Ny4wMjMiIGZpbGw9IiMwMDkxMkQiLz48L3N2Zz4=';
|
||||
|
||||
const { rottenTomatoes: rottenTomatoesRatings = {} as RatingValues } =
|
||||
ratings;
|
||||
const { value = 0 } = rottenTomatoesRatings;
|
||||
|
||||
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
||||
|
||||
return (
|
||||
<span>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
alt={translate('RottenTomatoesRating')}
|
||||
src={ratingImage}
|
||||
style={{
|
||||
height: `${iconSize}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{value}%
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default RottenTomatoRating;
|
||||
@@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from './Icon';
|
||||
|
||||
function SpinnerIcon(props) {
|
||||
const {
|
||||
name,
|
||||
spinningName,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Icon
|
||||
name={isSpinning ? (spinningName || name) : name}
|
||||
isSpinning={isSpinning}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SpinnerIcon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object.isRequired,
|
||||
isSpinning: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
SpinnerIcon.defaultProps = {
|
||||
spinningName: icons.SPINNER
|
||||
};
|
||||
|
||||
export default SpinnerIcon;
|
||||
21
frontend/src/Components/SpinnerIcon.tsx
Normal file
21
frontend/src/Components/SpinnerIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
export interface SpinnerIconProps extends IconProps {
|
||||
spinningName?: IconProps['name'];
|
||||
isSpinning: Required<IconProps['isSpinning']>;
|
||||
}
|
||||
|
||||
export default function SpinnerIcon({
|
||||
name,
|
||||
spinningName = icons.SPINNER,
|
||||
...otherProps
|
||||
}: SpinnerIconProps) {
|
||||
return (
|
||||
<Icon
|
||||
name={(otherProps.isSpinning && spinningName) || name}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './RelativeDateCell.css';
|
||||
|
||||
class RelativeDateCell extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
date,
|
||||
includeSeconds,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
component: Component,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (!date) {
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
|
||||
{...otherProps}
|
||||
>
|
||||
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RelativeDateCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
date: PropTypes.string,
|
||||
includeSeconds: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
component: PropTypes.elementType,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
RelativeDateCell.defaultProps = {
|
||||
className: styles.cell,
|
||||
includeSeconds: false,
|
||||
component: TableRowCell
|
||||
};
|
||||
|
||||
export default RelativeDateCell;
|
||||
59
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
59
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './RelativeDateCell.css';
|
||||
|
||||
interface RelativeDateCellProps {
|
||||
className?: string;
|
||||
date?: string;
|
||||
includeSeconds?: boolean;
|
||||
includeTime?: boolean;
|
||||
timeForToday?: boolean;
|
||||
component?: React.ElementType;
|
||||
}
|
||||
|
||||
function RelativeDateCell(props: RelativeDateCellProps) {
|
||||
const {
|
||||
className = styles.cell,
|
||||
date,
|
||||
includeSeconds = false,
|
||||
includeTime = false,
|
||||
timeForToday = true,
|
||||
|
||||
component: Component = TableRowCell,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
if (!date) {
|
||||
return <Component className={className} {...otherProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
title={formatDateTime(date, longDateFormat, timeFormat, {
|
||||
includeSeconds,
|
||||
includeRelativeDay: !showRelativeDates,
|
||||
})}
|
||||
{...otherProps}
|
||||
>
|
||||
{getRelativeDate({
|
||||
date,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
timeFormat,
|
||||
includeSeconds,
|
||||
includeTime,
|
||||
timeForToday,
|
||||
})}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
export default RelativeDateCell;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import RelativeDateCell from './RelativeDateCell';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, null)(RelativeDateCell);
|
||||
@@ -1,37 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './TableRowCell.css';
|
||||
|
||||
class TableRowCell extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<td
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableRowCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
};
|
||||
|
||||
TableRowCell.defaultProps = {
|
||||
className: styles.cell
|
||||
};
|
||||
|
||||
export default TableRowCell;
|
||||
11
frontend/src/Components/Table/Cells/TableRowCell.tsx
Normal file
11
frontend/src/Components/Table/Cells/TableRowCell.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import styles from './TableRowCell.css';
|
||||
|
||||
export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {}
|
||||
|
||||
export default function TableRowCell({
|
||||
className = styles.cell,
|
||||
...tdProps
|
||||
}: TableRowCellProps) {
|
||||
return <td className={className} {...tdProps} />;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
// TODO: Convert to generic so `name` can be a type
|
||||
interface Column {
|
||||
name: string;
|
||||
label: string | PropertyFunction<string> | React.ReactNode;
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import styles from './TmdbRating.css';
|
||||
|
||||
class TmdbRating extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
ratings,
|
||||
hideIcon,
|
||||
iconSize
|
||||
} = this.props;
|
||||
|
||||
const tmdbImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOTAuMjQgODEuNTIiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeTE9IjQwLjc2IiB4Mj0iMTkwLjI0IiB5Mj0iNDAuNzYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MGNlYTEiLz48c3RvcCBvZmZzZXQ9Ii41NiIgc3RvcC1jb2xvcj0iIzNjYmVjOSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwYjNlNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGQ9Ik0xMDUuNjcgMzYuMDZoNjYuOWExNy42NyAxNy42NyAwIDAwMTcuNjctMTcuNjZBMTcuNjcgMTcuNjcgMCAwMDE3Mi41Ny43M2gtNjYuOUExNy42NyAxNy42NyAwIDAwODggMTguNGExNy42NyAxNy42NyAwIDAwMTcuNjcgMTcuNjZ6bS04OCA0NWg3Ni45YTE3LjY3IDE3LjY3IDAgMDAxNy42Ny0xNy42NiAxNy42NyAxNy42NyAwIDAwLTE3LjY3LTE3LjY3aC03Ni45QTE3LjY3IDE3LjY3IDAgMDAwIDYzLjRhMTcuNjcgMTcuNjcgMCAwMDE3LjY3IDE3LjY2em0tNy4yNi00NS42NGg3LjhWNi45MmgxMC4xVjBoLTI4djYuOWgxMC4xem0yOC4xIDBoNy44VjguMjVoLjFsOSAyNy4xNWg2bDkuMy0yNy4xNWguMVYzNS40aDcuOFYwSDY2Ljc2bC04LjIgMjMuMWgtLjFMNTAuMzEgMGgtMTEuOHptMTEzLjkyIDIwLjI1YTE1LjA3IDE1LjA3IDAgMDAtNC41Mi01LjUyIDE4LjU3IDE4LjU3IDAgMDAtNi42OC0zLjA4IDMzLjU0IDMzLjU0IDAgMDAtOC4wNy0xaC0xMS43djM1LjRoMTIuNzVhMjQuNTggMjQuNTggMCAwMDcuNTUtMS4xNSAxOS4zNCAxOS4zNCAwIDAwNi4zNS0zLjMyIDE2LjI3IDE2LjI3IDAgMDA0LjM3LTUuNSAxNi45MSAxNi45MSAwIDAwMS42My03LjU4IDE4LjUgMTguNSAwIDAwLTEuNjgtOC4yNXpNMTQ1IDY4LjZhOC44IDguOCAwIDAxLTIuNjQgMy40IDEwLjcgMTAuNyAwIDAxLTQgMS44MiAyMS41NyAyMS41NyAwIDAxLTUgLjU1aC00LjA1di0yMWg0LjZhMTcgMTcgMCAwMTQuNjcuNjMgMTEuNjYgMTEuNjYgMCAwMTMuODggMS44N0E5LjE0IDkuMTQgMCAwMTE0NSA1OWE5Ljg3IDkuODcgMCAwMTEgNC41MiAxMS44OSAxMS44OSAwIDAxLTEgNS4wOHptNDQuNjMtLjEzYTggOCAwIDAwLTEuNTgtMi42MiA4LjM4IDguMzggMCAwMC0yLjQyLTEuODUgMTAuMzEgMTAuMzEgMCAwMC0zLjE3LTF2LS4xYTkuMjIgOS4yMiAwIDAwNC40Mi0yLjgyIDcuNDMgNy40MyAwIDAwMS42OC01IDguNDIgOC40MiAwIDAwLTEuMTUtNC42NSA4LjA5IDguMDkgMCAwMC0zLTIuNzIgMTIuNTYgMTIuNTYgMCAwMC00LjE4LTEuMyAzMi44NCAzMi44NCAwIDAwLTQuNjItLjMzaC0xMy4ydjM1LjRoMTQuNWEyMi40MSAyMi40MSAwIDAwNC43Mi0uNSAxMy41MyAxMy41MyAwIDAwNC4yOC0xLjY1IDkuNDIgOS40MiAwIDAwMy4xLTMgOC41MiA4LjUyIDAgMDAxLjItNC42OCA5LjM5IDkuMzkgMCAwMC0uNTUtMy4xOHptLTE5LjQyLTE1Ljc1aDUuM2ExMCAxMCAwIDAxMS44NS4xOCA2LjE4IDYuMTggMCAwMTEuNy41NyAzLjM5IDMuMzkgMCAwMTEuMjIgMS4xMyAzLjIyIDMuMjIgMCAwMS40OCAxLjgyIDMuNjMgMy42MyAwIDAxLS40MyAxLjggMy40IDMuNCAwIDAxLTEuMTIgMS4yIDQuOTIgNC45MiAwIDAxLTEuNTguNjUgNy41MSA3LjUxIDAgMDEtMS43Ny4yaC01LjY1em0xMS43MiAyMGEzLjkgMy45IDAgMDEtMS4yMiAxLjMgNC42NCA0LjY0IDAgMDEtMS42OC43IDguMTggOC4xOCAwIDAxLTEuODIuMmgtN3YtOGg1LjlhMTUuMzUgMTUuMzUgMCAwMTIgLjE1IDguNDcgOC40NyAwIDAxMi4wNS41NSA0IDQgMCAwMTEuNTcgMS4xOCAzLjExIDMuMTEgMCAwMS42MyAyIDMuNzEgMy43MSAwIDAxLS40MyAxLjkyeiIgZmlsbD0idXJsKCNhKSIvPjwvc3ZnPg==';
|
||||
|
||||
const rating = ratings.tmdb;
|
||||
|
||||
let ratingString = '0%';
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${(rating.value * 10).toFixed()}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span title={`${rating.votes} votes`}>
|
||||
{
|
||||
!hideIcon &&
|
||||
<img
|
||||
className={styles.image}
|
||||
src={tmdbImage}
|
||||
style={{
|
||||
height: `${iconSize}px`
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{ratingString}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TmdbRating.propTypes = {
|
||||
ratings: PropTypes.object.isRequired,
|
||||
iconSize: PropTypes.number.isRequired,
|
||||
hideIcon: PropTypes.bool
|
||||
};
|
||||
|
||||
TmdbRating.defaultProps = {
|
||||
iconSize: 14
|
||||
};
|
||||
|
||||
export default TmdbRating;
|
||||
46
frontend/src/Components/TmdbRating.tsx
Normal file
46
frontend/src/Components/TmdbRating.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { Ratings } from 'Movie/Movie';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TmdbRating.css';
|
||||
|
||||
interface TmdbRatingProps {
|
||||
ratings: Ratings;
|
||||
iconSize?: number;
|
||||
hideIcon?: boolean;
|
||||
}
|
||||
|
||||
function TmdbRating(props: TmdbRatingProps) {
|
||||
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||
|
||||
const tmdbImage =
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOTAuMjQgODEuNTIiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeTE9IjQwLjc2IiB4Mj0iMTkwLjI0IiB5Mj0iNDAuNzYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MGNlYTEiLz48c3RvcCBvZmZzZXQ9Ii41NiIgc3RvcC1jb2xvcj0iIzNjYmVjOSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwYjNlNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGQ9Ik0xMDUuNjcgMzYuMDZoNjYuOWExNy42NyAxNy42NyAwIDAwMTcuNjctMTcuNjZBMTcuNjcgMTcuNjcgMCAwMDE3Mi41Ny43M2gtNjYuOUExNy42NyAxNy42NyAwIDAwODggMTguNGExNy42NyAxNy42NyAwIDAwMTcuNjcgMTcuNjZ6bS04OCA0NWg3Ni45YTE3LjY3IDE3LjY3IDAgMDAxNy42Ny0xNy42NiAxNy42NyAxNy42NyAwIDAwLTE3LjY3LTE3LjY3aC03Ni45QTE3LjY3IDE3LjY3IDAgMDAwIDYzLjRhMTcuNjcgMTcuNjcgMCAwMDE3LjY3IDE3LjY2em0tNy4yNi00NS42NGg3LjhWNi45MmgxMC4xVjBoLTI4djYuOWgxMC4xem0yOC4xIDBoNy44VjguMjVoLjFsOSAyNy4xNWg2bDkuMy0yNy4xNWguMVYzNS40aDcuOFYwSDY2Ljc2bC04LjIgMjMuMWgtLjFMNTAuMzEgMGgtMTEuOHptMTEzLjkyIDIwLjI1YTE1LjA3IDE1LjA3IDAgMDAtNC41Mi01LjUyIDE4LjU3IDE4LjU3IDAgMDAtNi42OC0zLjA4IDMzLjU0IDMzLjU0IDAgMDAtOC4wNy0xaC0xMS43djM1LjRoMTIuNzVhMjQuNTggMjQuNTggMCAwMDcuNTUtMS4xNSAxOS4zNCAxOS4zNCAwIDAwNi4zNS0zLjMyIDE2LjI3IDE2LjI3IDAgMDA0LjM3LTUuNSAxNi45MSAxNi45MSAwIDAwMS42My03LjU4IDE4LjUgMTguNSAwIDAwLTEuNjgtOC4yNXpNMTQ1IDY4LjZhOC44IDguOCAwIDAxLTIuNjQgMy40IDEwLjcgMTAuNyAwIDAxLTQgMS44MiAyMS41NyAyMS41NyAwIDAxLTUgLjU1aC00LjA1di0yMWg0LjZhMTcgMTcgMCAwMTQuNjcuNjMgMTEuNjYgMTEuNjYgMCAwMTMuODggMS44N0E5LjE0IDkuMTQgMCAwMTE0NSA1OWE5Ljg3IDkuODcgMCAwMTEgNC41MiAxMS44OSAxMS44OSAwIDAxLTEgNS4wOHptNDQuNjMtLjEzYTggOCAwIDAwLTEuNTgtMi42MiA4LjM4IDguMzggMCAwMC0yLjQyLTEuODUgMTAuMzEgMTAuMzEgMCAwMC0zLjE3LTF2LS4xYTkuMjIgOS4yMiAwIDAwNC40Mi0yLjgyIDcuNDMgNy40MyAwIDAwMS42OC01IDguNDIgOC40MiAwIDAwLTEuMTUtNC42NSA4LjA5IDguMDkgMCAwMC0zLTIuNzIgMTIuNTYgMTIuNTYgMCAwMC00LjE4LTEuMyAzMi44NCAzMi44NCAwIDAwLTQuNjItLjMzaC0xMy4ydjM1LjRoMTQuNWEyMi40MSAyMi40MSAwIDAwNC43Mi0uNSAxMy41MyAxMy41MyAwIDAwNC4yOC0xLjY1IDkuNDIgOS40MiAwIDAwMy4xLTMgOC41MiA4LjUyIDAgMDAxLjItNC42OCA5LjM5IDkuMzkgMCAwMC0uNTUtMy4xOHptLTE5LjQyLTE1Ljc1aDUuM2ExMCAxMCAwIDAxMS44NS4xOCA2LjE4IDYuMTggMCAwMTEuNy41NyAzLjM5IDMuMzkgMCAwMTEuMjIgMS4xMyAzLjIyIDMuMjIgMCAwMS40OCAxLjgyIDMuNjMgMy42MyAwIDAxLS40MyAxLjggMy40IDMuNCAwIDAxLTEuMTIgMS4yIDQuOTIgNC45MiAwIDAxLTEuNTguNjUgNy41MSA3LjUxIDAgMDEtMS43Ny4yaC01LjY1em0xMS43MiAyMGEzLjkgMy45IDAgMDEtMS4yMiAxLjMgNC42NCA0LjY0IDAgMDEtMS42OC43IDguMTggOC4xOCAwIDAxLTEuODIuMmgtN3YtOGg1LjlhMTUuMzUgMTUuMzUgMCAwMTIgLjE1IDguNDcgOC40NyAwIDAxMi4wNS41NSA0IDQgMCAwMTEuNTcgMS4xOCAzLjExIDMuMTEgMCAwMS42MyAyIDMuNzEgMy43MSAwIDAxLS40MyAxLjkyeiIgZmlsbD0idXJsKCNhKSIvPjwvc3ZnPg==';
|
||||
|
||||
const { value = 0, votes = 0 } = ratings.tmdb;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
alt={translate('TmdbRating')}
|
||||
src={tmdbImage}
|
||||
style={{
|
||||
height: `${iconSize}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(value * 10).toFixed()}%
|
||||
</span>
|
||||
}
|
||||
tooltip={translate('CountVotes', { votes })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TmdbRating;
|
||||
@@ -56,15 +56,6 @@ function DiscoverMovieSortMenu(props) {
|
||||
{translate('InCinemas')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="physicalRelease"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('PhysicalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="digitalRelease"
|
||||
sortKey={sortKey}
|
||||
@@ -74,6 +65,15 @@ function DiscoverMovieSortMenu(props) {
|
||||
{translate('DigitalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="physicalRelease"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('PhysicalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="runtime"
|
||||
sortKey={sortKey}
|
||||
@@ -84,12 +84,30 @@ function DiscoverMovieSortMenu(props) {
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="ratings"
|
||||
name="tmdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Rating')}
|
||||
{translate('TmdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="imdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('ImdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="rottenTomatoesRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('RottenTomatoesRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -14,25 +15,30 @@ const rows = [
|
||||
showProp: 'showYear',
|
||||
valueProp: 'year'
|
||||
},
|
||||
{
|
||||
name: 'studio',
|
||||
showProp: 'showStudio',
|
||||
valueProp: 'studio'
|
||||
},
|
||||
{
|
||||
name: 'genres',
|
||||
showProp: 'showGenres',
|
||||
valueProp: 'genres'
|
||||
},
|
||||
{
|
||||
name: 'ratings',
|
||||
showProp: 'showRatings',
|
||||
valueProp: 'ratings'
|
||||
name: 'tmdbRating',
|
||||
showProp: 'showTmdbRating',
|
||||
valueProp: 'ratings.tmdb.value'
|
||||
},
|
||||
{
|
||||
name: 'imdbRating',
|
||||
showProp: 'showImdbRating',
|
||||
valueProp: 'ratings.imdb.value'
|
||||
},
|
||||
{
|
||||
name: 'certification',
|
||||
showProp: 'showCertification',
|
||||
valueProp: 'certification'
|
||||
},
|
||||
{
|
||||
name: 'studio',
|
||||
showProp: 'showStudio',
|
||||
valueProp: 'studio'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -43,11 +49,7 @@ function isVisible(row, props) {
|
||||
valueProp
|
||||
} = row;
|
||||
|
||||
if (props[valueProp] == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return props[showProp] || props.sortKey === name;
|
||||
return _.has(props, valueProp) && (_.get(props, showProp) || props.sortKey === name);
|
||||
}
|
||||
|
||||
function getInfoRowProps(row, props) {
|
||||
@@ -61,6 +63,14 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'studio') {
|
||||
return {
|
||||
title: translate('Studio'),
|
||||
iconName: icons.STUDIO,
|
||||
label: props.studio
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'genres') {
|
||||
return {
|
||||
title: translate('Genres'),
|
||||
@@ -69,14 +79,22 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
if (name === 'tmdbRating' && !!props.ratings.tmdb) {
|
||||
return {
|
||||
title: translate('Ratings'),
|
||||
title: translate('TmdbRating'),
|
||||
iconName: icons.HEART,
|
||||
label: `${(props.ratings.tmdb.value * 10).toFixed()}%`
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'imdbRating' && !!props.ratings.imdb) {
|
||||
return {
|
||||
title: translate('ImdbRating'),
|
||||
iconName: icons.IMDB,
|
||||
label: `${(props.ratings.imdb.value).toFixed(1)}`
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'certification') {
|
||||
return {
|
||||
title: translate('Certification'),
|
||||
@@ -84,14 +102,6 @@ function getInfoRowProps(row, props) {
|
||||
label: props.certification
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'studio') {
|
||||
return {
|
||||
title: translate('Studio'),
|
||||
iconName: icons.STUDIO,
|
||||
label: props.studio
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function DiscoverMovieOverviewInfo(props) {
|
||||
@@ -132,11 +142,12 @@ function DiscoverMovieOverviewInfo(props) {
|
||||
|
||||
DiscoverMovieOverviewInfo.propTypes = {
|
||||
height: PropTypes.number.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showYear: PropTypes.bool.isRequired,
|
||||
showRatings: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showGenres: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
studio: PropTypes.string,
|
||||
year: PropTypes.number,
|
||||
certification: PropTypes.string,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './DiscoverMovieOverviewInfoRow.css';
|
||||
|
||||
function DiscoverMovieOverviewInfoRow(props) {
|
||||
const {
|
||||
title,
|
||||
iconName,
|
||||
label
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.infoRow}
|
||||
title={title}
|
||||
>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
name={iconName}
|
||||
size={14}
|
||||
/>
|
||||
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DiscoverMovieOverviewInfoRow.propTypes = {
|
||||
title: PropTypes.string,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
|
||||
};
|
||||
|
||||
export default DiscoverMovieOverviewInfoRow;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from 'Components/Icon';
|
||||
import styles from './DiscoverMovieOverviewInfoRow.css';
|
||||
|
||||
interface DiscoverMovieOverviewInfoRowProps {
|
||||
title?: string;
|
||||
iconName: IconProps['name'];
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
function DiscoverMovieOverviewInfoRow(
|
||||
props: DiscoverMovieOverviewInfoRowProps
|
||||
) {
|
||||
const { title, iconName, label } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.infoRow} title={title}>
|
||||
<Icon className={styles.icon} name={iconName} size={14} />
|
||||
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiscoverMovieOverviewInfoRow;
|
||||
@@ -65,7 +65,8 @@ class DiscoverMovieOverviews extends Component {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter
|
||||
jumpToCharacter,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -75,13 +76,17 @@ class DiscoverMovieOverviews extends Component {
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions) {
|
||||
this.calculateGrid();
|
||||
this.calculateGrid(this.state.width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (this._grid &&
|
||||
if (
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId'))) {
|
||||
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId') ||
|
||||
prevProps.overviewOptions !== overviewOptions
|
||||
)
|
||||
) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
|
||||
this.state = {
|
||||
size: props.size,
|
||||
showStudio: props.showStudio,
|
||||
showCertification: props.showCertification,
|
||||
showRatings: props.showRatings,
|
||||
showYear: props.showYear,
|
||||
showStudio: props.showStudio,
|
||||
showGenres: props.showGenres,
|
||||
showTmdbRating: props.showTmdbRating,
|
||||
showImdbRating: props.showImdbRating,
|
||||
showCertification: props.showCertification,
|
||||
includeRecommendations: props.includeRecommendations,
|
||||
includeTrending: props.includeTrending,
|
||||
includePopular: props.includePopular
|
||||
@@ -58,11 +59,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
size,
|
||||
showStudio,
|
||||
showYear,
|
||||
showRatings,
|
||||
showCertification,
|
||||
showStudio,
|
||||
showGenres,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showCertification,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -74,26 +76,30 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
state.size = size;
|
||||
}
|
||||
|
||||
if (showStudio !== prevProps.showStudio) {
|
||||
state.showStudio = showStudio;
|
||||
}
|
||||
|
||||
if (showYear !== prevProps.showYear) {
|
||||
state.showYear = showYear;
|
||||
}
|
||||
|
||||
if (showRatings !== prevProps.showRatings) {
|
||||
state.showRatings = showRatings;
|
||||
}
|
||||
|
||||
if (showCertification !== prevProps.showCertification) {
|
||||
state.showCertification = showCertification;
|
||||
if (showStudio !== prevProps.showStudio) {
|
||||
state.showStudio = showStudio;
|
||||
}
|
||||
|
||||
if (showGenres !== prevProps.showGenres) {
|
||||
state.showGenres = showGenres;
|
||||
}
|
||||
|
||||
if (showTmdbRating !== prevProps.showTmdbRating) {
|
||||
state.showTmdbRating = showTmdbRating;
|
||||
}
|
||||
|
||||
if (showImdbRating !== prevProps.showImdbRating) {
|
||||
state.showImdbRating = showImdbRating;
|
||||
}
|
||||
|
||||
if (showCertification !== prevProps.showCertification) {
|
||||
state.showCertification = showCertification;
|
||||
}
|
||||
|
||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||
state.includeRecommendations = includeRecommendations;
|
||||
}
|
||||
@@ -142,11 +148,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
|
||||
const {
|
||||
size,
|
||||
showStudio,
|
||||
showCertification,
|
||||
showRatings,
|
||||
showYear,
|
||||
showStudio,
|
||||
showGenres,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showCertification,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -209,12 +216,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowGenres')}</FormLabel>
|
||||
<FormLabel>{translate('ShowYear')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showGenres"
|
||||
value={showGenres}
|
||||
name="showYear"
|
||||
value={showYear}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -231,23 +238,34 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowYear')}</FormLabel>
|
||||
<FormLabel>{translate('ShowGenres')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showYear"
|
||||
value={showYear}
|
||||
name="showGenres"
|
||||
value={showGenres}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowRatings')}</FormLabel>
|
||||
<FormLabel>{translate('ShowTmdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showRatings"
|
||||
value={showRatings}
|
||||
name="showTmdbRating"
|
||||
value={showTmdbRating}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowImdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showImdbRating"
|
||||
value={showImdbRating}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -279,11 +297,12 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
|
||||
DiscoverMovieOverviewOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showYear: PropTypes.bool.isRequired,
|
||||
showRatings: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
showStudio: PropTypes.bool.isRequired,
|
||||
showGenres: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showCertification: PropTypes.bool.isRequired,
|
||||
includeRecommendations: PropTypes.bool.isRequired,
|
||||
includeTrending: PropTypes.bool.isRequired,
|
||||
includePopular: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -2,9 +2,12 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
@@ -86,6 +89,10 @@ class DiscoverMoviePoster extends Component {
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
ratings,
|
||||
isExisting,
|
||||
isExcluded,
|
||||
isSelected,
|
||||
@@ -192,18 +199,39 @@ class DiscoverMoviePoster extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{
|
||||
showTitle &&
|
||||
<div className={styles.title} title={title}>
|
||||
{title}
|
||||
</div>
|
||||
}
|
||||
{showTitle ?
|
||||
<div className={styles.title} title={title}>
|
||||
{title}
|
||||
</div> :
|
||||
null}
|
||||
|
||||
{showTmdbRating && !!ratings.tmdb ? (
|
||||
<div className={styles.title}>
|
||||
<TmdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showImdbRating && !!ratings.imdb ? (
|
||||
<div className={styles.title}>
|
||||
<ImdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showRottenTomatoesRating && !!ratings.rottenTomatoes ? (
|
||||
<div className={styles.title}>
|
||||
<RottenTomatoRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DiscoverMoviePosterInfo
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
movieRuntimeFormat={movieRuntimeFormat}
|
||||
ratings={ratings}
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -243,6 +271,10 @@ DiscoverMoviePoster.propTypes = {
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
@@ -23,7 +25,10 @@ function DiscoverMoviePosterInfo(props) {
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
movieRuntimeFormat
|
||||
movieRuntimeFormat,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
} = props;
|
||||
|
||||
if (sortKey === 'status' && status) {
|
||||
@@ -112,10 +117,26 @@ function DiscoverMoviePosterInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'ratings' && ratings) {
|
||||
if (!showTmdbRating && sortKey === 'tmdbRating' && !!ratings.tmdb) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<TmdbRating ratings={ratings} />
|
||||
<TmdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showImdbRating && sortKey === 'imdbRating' && !!ratings.imdb) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<ImdbRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showRottenTomatoesRating && sortKey === 'rottenTomatoesRating' && !!ratings.rottenTomatoes) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<RottenTomatoRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -136,7 +157,10 @@ DiscoverMoviePosterInfo.propTypes = {
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default DiscoverMoviePosterInfo;
|
||||
|
||||
@@ -36,15 +36,15 @@ function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
} = posterOptions;
|
||||
|
||||
const nextAiringHeight = 19;
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
nextAiringHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
@@ -52,8 +52,42 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTmdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showImdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showRottenTomatoesRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'studio':
|
||||
case 'inCinemas':
|
||||
case 'digitalRelease':
|
||||
case 'physicalRelease':
|
||||
case 'runtime':
|
||||
case 'certification':
|
||||
heights.push(19);
|
||||
break;
|
||||
case 'tmdbRating':
|
||||
if (!showTmdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'imdbRating':
|
||||
if (!showImdbRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'rottenTomatoesRating':
|
||||
if (!showRottenTomatoesRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
@@ -182,7 +216,10 @@ class DiscoverMoviePosters extends Component {
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
showTitle
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
} = posterOptions;
|
||||
|
||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||
@@ -208,6 +245,9 @@ class DiscoverMoviePosters extends Component {
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
showTitle={showTitle}
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
|
||||
@@ -45,6 +45,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
this.state = {
|
||||
size: props.size,
|
||||
showTitle: props.showTitle,
|
||||
showTmdbRating: props.showTmdbRating,
|
||||
showImdbRating: props.showImdbRating,
|
||||
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
||||
includeRecommendations: props.includeRecommendations,
|
||||
includeTrending: props.includeTrending,
|
||||
includePopular: props.includePopular
|
||||
@@ -55,6 +58,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
const {
|
||||
size,
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -70,6 +76,18 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
state.showTitle = showTitle;
|
||||
}
|
||||
|
||||
if (showTmdbRating !== prevProps.showTmdbRating) {
|
||||
state.showTmdbRating = showTmdbRating;
|
||||
}
|
||||
|
||||
if (showImdbRating !== prevProps.showImdbRating) {
|
||||
state.showImdbRating = showImdbRating;
|
||||
}
|
||||
|
||||
if (showRottenTomatoesRating !== prevProps.showRottenTomatoesRating) {
|
||||
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
||||
}
|
||||
|
||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||
state.includeRecommendations = includeRecommendations;
|
||||
}
|
||||
@@ -119,6 +137,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
const {
|
||||
size,
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -191,6 +212,42 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowTmdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTmdbRating"
|
||||
value={showTmdbRating}
|
||||
helpText={translate('ShowTmdbRatingHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowImdbRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showImdbRating"
|
||||
value={showImdbRating}
|
||||
helpText={translate('ShowImdbRatingHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowRottenTomatoesRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showRottenTomatoesRating"
|
||||
value={showRottenTomatoesRating}
|
||||
helpText={translate('ShowRottenTomatoesRatingHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
@@ -209,6 +266,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
DiscoverMoviePosterOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
includeRecommendations: PropTypes.bool.isRequired,
|
||||
includeTrending: PropTypes.bool.isRequired,
|
||||
includePopular: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.ratings,
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.runtime {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CssExports {
|
||||
'collection': string;
|
||||
'digitalRelease': string;
|
||||
'genres': string;
|
||||
'imdbRating': string;
|
||||
'inCinemas': string;
|
||||
'isPopular': string;
|
||||
'isRecommendation': string;
|
||||
@@ -14,11 +15,12 @@ interface CssExports {
|
||||
'originalLanguage': string;
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'ratings': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sortTitle': string;
|
||||
'status': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.ratings,
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.runtime {
|
||||
composes: cell;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ interface CssExports {
|
||||
'exclusionIcon': string;
|
||||
'externalLinks': string;
|
||||
'genres': string;
|
||||
'imdbRating': string;
|
||||
'inCinemas': string;
|
||||
'isPopular': string;
|
||||
'isRecommendation': string;
|
||||
@@ -19,12 +20,13 @@ interface CssExports {
|
||||
'originalLanguage': string;
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'ratings': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sortTitle': string;
|
||||
'status': string;
|
||||
'statusIcon': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import ImportListListConnector from 'Components/ImportListListConnector';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
@@ -196,10 +198,11 @@ class DiscoverMovieRow extends Component {
|
||||
|
||||
if (name === 'inCinemas') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={inCinemas}
|
||||
timeForToday={false}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
@@ -207,10 +210,11 @@ class DiscoverMovieRow extends Component {
|
||||
|
||||
if (name === 'physicalRelease') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={physicalRelease}
|
||||
timeForToday={false}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
@@ -218,10 +222,11 @@ class DiscoverMovieRow extends Component {
|
||||
|
||||
if (name === 'digitalRelease') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={digitalRelease}
|
||||
timeForToday={false}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
@@ -253,15 +258,35 @@ class DiscoverMovieRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
if (name === 'tmdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
{ratings.tmdb ? <TmdbRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'imdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{ratings.imdb ? <ImdbRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'rottenTomatoesRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{ratings.rottenTomatoes ? <RottenTomatoRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
enum DownloadProtocol {
|
||||
Unknown = 'unknown',
|
||||
Usenet = 'usenet',
|
||||
Torrent = 'torrent',
|
||||
}
|
||||
type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
|
||||
|
||||
export default DownloadProtocol;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
||||
27
frontend/src/FirstRun/AuthenticationRequiredModal.tsx
Normal file
27
frontend/src/FirstRun/AuthenticationRequiredModal.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
interface AuthenticationRequiredModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function AuthenticationRequiredModal({
|
||||
isOpen,
|
||||
}: AuthenticationRequiredModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
dispatchFetchStatus
|
||||
} = props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && didMount.current) {
|
||||
dispatchFetchStatus();
|
||||
}
|
||||
|
||||
didMount.current = true;
|
||||
}, [isSaving, dispatchFetchStatus]);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('AuthenticationRequired')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModalContent;
|
||||
194
frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx
Normal file
194
frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
authenticationMethodOptions,
|
||||
authenticationRequiredOptions,
|
||||
} from 'Settings/General/SecuritySettings';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchGeneralSettings,
|
||||
saveGeneralSettings,
|
||||
setGeneralSettingsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
const selector = createSettingsSectionSelector(SECTION);
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
export default function AuthenticationRequiredModalContent() {
|
||||
const { isPopulated, error, isSaving, settings } = useSelector(selector);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
} = settings;
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchGeneralSettings());
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(args: InputChanged) => {
|
||||
// @ts-expect-error Actions aren't typed
|
||||
dispatch(setGeneralSettingsValue(args));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const authenticationEnabled =
|
||||
authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
useEffect(() => {
|
||||
if (isSaving || !wasSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchStatus());
|
||||
}, [isSaving, wasSaving, dispatch]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
dispatch(saveGeneralSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent showCloseButton={false} onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AuthenticationRequired')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert className={styles.authRequiredAlert} kind={kinds.WARNING}>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{isPopulated && !error ? (
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={
|
||||
authenticationMethod.value === 'none'
|
||||
? translate('AuthenticationMethodHelpTextWarning')
|
||||
: undefined
|
||||
}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
helpTextWarning={
|
||||
username?.value
|
||||
? undefined
|
||||
: translate('AuthenticationRequiredUsernameHelpTextWarning')
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
helpTextWarning={
|
||||
password?.value
|
||||
? undefined
|
||||
: translate('AuthenticationRequiredPasswordHelpTextWarning')
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
helpTextWarning={
|
||||
passwordConfirmation?.value
|
||||
? undefined
|
||||
: translate(
|
||||
'AuthenticationRequiredPasswordConfirmationHelpTextWarning'
|
||||
)
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onPress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user