1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

13 Commits

Author SHA1 Message Date
Robin Dadswell d63b3ca59d fixed issues with test 2024-11-19 15:29:49 +00:00
Robin Dadswell c8faab9928 fixup tests
local build not working, need to work out why - it's to do with a nuget restore
2024-11-18 17:16:18 +00:00
Robin Dadswell 0a6b0ee959 fix macos image version 2024-11-18 15:53:45 +00:00
Robin Dadswell b993f70d2c Version Bump to force build 2024-11-18 15:48:21 +00:00
Qstick 85544ca8f6 fixup! New: Multiple Quality Profiles and Files Per Movie 2023-10-14 20:42:57 -05:00
Qstick 9e7ad678b0 Add api endpoint to generate the required login cookie
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2023-10-14 17:44:48 -05:00
Robin Dadswell 0aebd90ac9 Fixed: Another attempted fix on ODIC correlation error 2023-10-14 17:44:48 -05:00
Robin Dadswell 76bed80060 Fixed: Alternative fix on ODIC correlation error 2023-10-14 17:44:48 -05:00
Robin Dadswell 669b50dc72 New: SSO goes straight to authentication provider 2023-10-14 17:44:48 -05:00
ta264 18fc1413c3 New: OIDC and Plex authentication methods
(cherry picked from commit 3ff3de6b90704fba266833115cd9d03ace99aae9)
2023-10-14 17:44:47 -05:00
Qstick 775b1ba9cf Build Branch [REVERT] 2023-10-14 17:44:47 -05:00
Qstick 5ad3f96e0f New: Multiple Quality Profiles and Files Per Movie 2023-10-14 17:44:47 -05:00
Qstick b024fcf5ee Bump Version to 6 2023-10-14 17:36:43 -05:00
353 changed files with 4763 additions and 3106 deletions
+3 -2
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.1.1' majorVersion: '6.0.1'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -20,13 +20,14 @@ variables:
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11' macImage: 'macOS-13'
trigger: trigger:
branches: branches:
include: include:
- develop - develop
- master - master
- zeus
paths: paths:
exclude: exclude:
- .github - .github
+2 -6
View File
@@ -14,7 +14,6 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector'; import HistoryRowConnector from './HistoryRowConnector';
class History extends Component { class History extends Component {
@@ -34,7 +33,6 @@ class History extends Component {
columns, columns,
selectedFilterKey, selectedFilterKey,
filters, filters,
customFilters,
totalRecords, totalRecords,
onFilterSelect, onFilterSelect,
onFirstPagePress, onFirstPagePress,
@@ -72,8 +70,7 @@ class History extends Component {
alignMenu={align.RIGHT} alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={customFilters} customFilters={[]}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
</PageToolbarSection> </PageToolbarSection>
@@ -147,9 +144,8 @@ History.propTypes = {
moviesError: PropTypes.object, moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number, totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired onFirstPagePress: PropTypes.func.isRequired
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions'; import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History'; import History from './History';
@@ -12,13 +11,11 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.history, (state) => state.history,
(state) => state.movies, (state) => state.movies,
createCustomFiltersSelector('history'), (history, movies) => {
(history, movies, customFilters) => {
return { return {
isMoviesFetching: movies.isFetching, isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated, isMoviesPopulated: movies.isPopulated,
moviesError: movies.error, moviesError: movies.error,
customFilters,
...history ...history
}; };
} }
@@ -1,54 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setHistoryFilter } from 'Store/Actions/historyActions';
function createHistorySelector() {
return createSelector(
(state: AppState) => state.history.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.history.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface HistoryFilterModalProps {
isOpen: boolean;
}
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
const sectionItems = useSelector(createHistorySelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'history';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setHistoryFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}
+2 -31
View File
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -22,7 +21,6 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueFilterModal from './QueueFilterModal';
import QueueOptionsConnector from './QueueOptionsConnector'; import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector'; import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal'; import RemoveQueueItemsModal from './RemoveQueueItemsModal';
@@ -155,16 +153,11 @@ class Queue extends Component {
isMoviesPopulated, isMoviesPopulated,
moviesError, moviesError,
columns, columns,
selectedFilterKey,
filters,
customFilters,
count,
totalRecords, totalRecords,
isGrabbing, isGrabbing,
isRemoving, isRemoving,
isRefreshMonitoredDownloadsExecuting, isRefreshMonitoredDownloadsExecuting,
onRefreshPress, onRefreshPress,
onFilterSelect,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -227,15 +220,6 @@ class Queue extends Component {
iconName={icons.TABLE} iconName={icons.TABLE}
/> />
</TableOptionsModalWrapper> </TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={QueueFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection> </PageToolbarSection>
</PageToolbar> </PageToolbar>
@@ -257,11 +241,7 @@ class Queue extends Component {
{ {
isAllPopulated && !hasError && !items.length ? isAllPopulated && !hasError && !items.length ?
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
{ {translate('QueueIsEmpty')}
selectedFilterKey !== 'all' && count > 0 ?
translate('QueueFilterHasNoItems') :
translate('QueueIsEmpty')
}
</Alert> : </Alert> :
null null
} }
@@ -345,22 +325,13 @@ Queue.propTypes = {
moviesError: PropTypes.object, moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: 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, totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired, isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired, onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired, onRemoveSelectedPress: PropTypes.func.isRequired
onFilterSelect: PropTypes.func.isRequired
};
Queue.defaultProps = {
count: 0
}; };
export default Queue; export default Queue;
+1 -12
View File
@@ -6,7 +6,6 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import * as queueActions from 'Store/Actions/queueActions'; import * as queueActions from 'Store/Actions/queueActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue'; import Queue from './Queue';
@@ -16,16 +15,12 @@ function createMapStateToProps() {
(state) => state.movies, (state) => state.movies,
(state) => state.queue.options, (state) => state.queue.options,
(state) => state.queue.paged, (state) => state.queue.paged,
(state) => state.queue.status.item,
createCustomFiltersSelector('queue'),
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS), createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => { (movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
return { return {
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
isMoviesFetching: movies.isFetching, isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated, isMoviesPopulated: movies.isPopulated,
moviesError: movies.error, moviesError: movies.error,
customFilters,
isRefreshMonitoredDownloadsExecuting, isRefreshMonitoredDownloadsExecuting,
...options, ...options,
...queue ...queue
@@ -111,10 +106,6 @@ class QueueConnector extends Component {
this.props.setQueueSort({ sortKey }); this.props.setQueueSort({ sortKey });
}; };
onFilterSelect = (selectedFilterKey) => {
this.props.setQueueFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => { onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload); this.props.setQueueTableOption(payload);
@@ -149,7 +140,6 @@ class QueueConnector extends Component {
onLastPagePress={this.onLastPagePress} onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect} onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress} onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange} onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress} onRefreshPress={this.onRefreshPress}
onGrabSelectedPress={this.onGrabSelectedPress} onGrabSelectedPress={this.onGrabSelectedPress}
@@ -172,7 +162,6 @@ QueueConnector.propTypes = {
gotoQueueLastPage: PropTypes.func.isRequired, gotoQueueLastPage: PropTypes.func.isRequired,
gotoQueuePage: PropTypes.func.isRequired, gotoQueuePage: PropTypes.func.isRequired,
setQueueSort: PropTypes.func.isRequired, setQueueSort: PropTypes.func.isRequired,
setQueueFilter: PropTypes.func.isRequired,
setQueueTableOption: PropTypes.func.isRequired, setQueueTableOption: PropTypes.func.isRequired,
clearQueue: PropTypes.func.isRequired, clearQueue: PropTypes.func.isRequired,
grabQueueItems: PropTypes.func.isRequired, grabQueueItems: PropTypes.func.isRequired,
@@ -1,54 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setQueueFilter } from 'Store/Actions/queueActions';
function createQueueSelector() {
return createSelector(
(state: AppState) => state.queue.paged.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.queue.paged.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface QueueFilterModalProps {
isOpen: boolean;
}
export default function QueueFilterModal(props: QueueFilterModalProps) {
const sectionItems = useSelector(createQueueSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'queue';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setQueueFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}
@@ -20,10 +20,6 @@ class AddNewMovieModalContent extends Component {
// //
// Listeners // Listeners
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
};
onAddMoviePress = () => { onAddMoviePress = () => {
this.props.onAddMoviePress(); this.props.onAddMoviePress();
}; };
@@ -40,7 +36,7 @@ class AddNewMovieModalContent extends Component {
isAdding, isAdding,
rootFolderPath, rootFolderPath,
monitor, monitor,
qualityProfileId, qualityProfileIds,
minimumAvailability, minimumAvailability,
searchForMovie, searchForMovie,
folder, folder,
@@ -130,9 +126,9 @@ class AddNewMovieModalContent extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId" name="qualityProfileIds"
onChange={this.onQualityProfileIdChange} onChange={onInputChange}
{...qualityProfileId} {...qualityProfileIds}
/> />
</FormGroup> </FormGroup>
@@ -189,7 +185,7 @@ AddNewMovieModalContent.propTypes = {
addError: PropTypes.object, addError: PropTypes.object,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired, searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired, folder: PropTypes.string.isRequired,
@@ -58,7 +58,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId, tmdbId,
rootFolderPath, rootFolderPath,
monitor, monitor,
qualityProfileId, qualityProfileIds,
minimumAvailability, minimumAvailability,
searchForMovie, searchForMovie,
tags tags
@@ -68,7 +68,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId, tmdbId,
rootFolderPath: rootFolderPath.value, rootFolderPath: rootFolderPath.value,
monitor: monitor.value, monitor: monitor.value,
qualityProfileId: qualityProfileId.value, qualityProfileIds: qualityProfileIds.value,
minimumAvailability: minimumAvailability.value, minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value, searchForMovie: searchForMovie.value,
tags: tags.value tags: tags.value
@@ -93,7 +93,7 @@ AddNewMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired, tmdbId: PropTypes.number.isRequired,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired, searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,
@@ -72,15 +72,19 @@ class AddNewMovieSearchResult extends Component {
colorImpairedMode, colorImpairedMode,
id, id,
monitored, monitored,
hasFile,
isAvailable, isAvailable,
queueStatus, queueStatus,
queueState, queueState,
runtime, runtime,
movieRuntimeFormat, movieRuntimeFormat,
certification certification,
statistics
} = this.props; } = this.props;
const {
movieFileCount
} = statistics;
const { const {
isNewAddMovieModalOpen isNewAddMovieModalOpen
} = this.state; } = this.state;
@@ -121,7 +125,7 @@ class AddNewMovieSearchResult extends Component {
isExistingMovie && isExistingMovie &&
<MovieIndexProgressBar <MovieIndexProgressBar
monitored={monitored} monitored={monitored}
hasFile={hasFile} hasFile={movieFileCount > 0}
status={status} status={status}
width={posterWidth} width={posterWidth}
detailedProgressBar={true} detailedProgressBar={true}
@@ -234,7 +238,7 @@ class AddNewMovieSearchResult extends Component {
{ {
isExistingMovie && isSmallScreen && isExistingMovie && isSmallScreen &&
<MovieStatusLabel <MovieStatusLabel
hasMovieFiles={hasFile} hasMovieFiles={movieFileCount > 0}
monitored={monitored} monitored={monitored}
isAvailable={isAvailable} isAvailable={isAvailable}
id={id} id={id}
@@ -291,7 +295,14 @@ AddNewMovieSearchResult.propTypes = {
queueState: PropTypes.string, queueState: PropTypes.string,
runtime: PropTypes.number.isRequired, runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired, movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string certification: PropTypes.string,
statistics: PropTypes.object
};
AddNewMovieSearchResult.defaultProps = {
statistics: {
movieFileCount: 0
}
}; };
export default AddNewMovieSearchResult; export default AddNewMovieSearchResult;
@@ -25,13 +25,13 @@ class ImportMovieFooter extends Component {
const { const {
defaultMonitor, defaultMonitor,
defaultQualityProfileId, defaultQualityProfileIds,
defaultMinimumAvailability defaultMinimumAvailability
} = props; } = props;
this.state = { this.state = {
monitor: defaultMonitor, monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId, qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability minimumAvailability: defaultMinimumAvailability
}; };
} }
@@ -39,16 +39,16 @@ class ImportMovieFooter extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { const {
defaultMonitor, defaultMonitor,
defaultQualityProfileId, defaultQualityProfileIds,
defaultMinimumAvailability, defaultMinimumAvailability,
isMonitorMixed, isMonitorMixed,
isQualityProfileIdMixed, isQualityProfileIdsMixed,
isMinimumAvailabilityMixed isMinimumAvailabilityMixed
} = this.props; } = this.props;
const { const {
monitor, monitor,
qualityProfileId, qualityProfileIds,
minimumAvailability minimumAvailability
} = this.state; } = this.state;
@@ -60,10 +60,10 @@ class ImportMovieFooter extends Component {
newState.monitor = defaultMonitor; newState.monitor = defaultMonitor;
} }
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) { if (isQualityProfileIdsMixed && qualityProfileIds !== MIXED) {
newState.qualityProfileId = MIXED; newState.qualityProfileIds = MIXED;
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) { } else if (!isQualityProfileIdsMixed && qualityProfileIds !== defaultQualityProfileIds) {
newState.qualityProfileId = defaultQualityProfileId; newState.qualityProfileIds = defaultQualityProfileIds;
} }
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) { if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
@@ -94,7 +94,7 @@ class ImportMovieFooter extends Component {
isImporting, isImporting,
isLookingUpMovie, isLookingUpMovie,
isMonitorMixed, isMonitorMixed,
isQualityProfileIdMixed, isQualityProfileIdsMixed,
isMinimumAvailabilityMixed, isMinimumAvailabilityMixed,
hasUnsearchedItems, hasUnsearchedItems,
importError, importError,
@@ -105,7 +105,7 @@ class ImportMovieFooter extends Component {
const { const {
monitor, monitor,
qualityProfileId, qualityProfileIds,
minimumAvailability minimumAvailability
} = this.state; } = this.state;
@@ -148,10 +148,10 @@ class ImportMovieFooter extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId" name="qualityProfileIds"
value={qualityProfileId} value={qualityProfileIds}
isDisabled={!selectedCount} isDisabled={!selectedCount}
includeMixed={isQualityProfileIdMixed} includeMixed={isQualityProfileIdsMixed}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
</div> </div>
@@ -257,10 +257,10 @@ ImportMovieFooter.propTypes = {
isImporting: PropTypes.bool.isRequired, isImporting: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired, isLookingUpMovie: PropTypes.bool.isRequired,
defaultMonitor: PropTypes.string.isRequired, defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number, defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultMinimumAvailability: PropTypes.string, defaultMinimumAvailability: PropTypes.string,
isMonitorMixed: PropTypes.bool.isRequired, isMonitorMixed: PropTypes.bool.isRequired,
isQualityProfileIdMixed: PropTypes.bool.isRequired, isQualityProfileIdsMixed: PropTypes.bool.isRequired,
isMinimumAvailabilityMixed: PropTypes.bool.isRequired, isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
hasUnsearchedItems: PropTypes.bool.isRequired, hasUnsearchedItems: PropTypes.bool.isRequired,
importError: PropTypes.object, importError: PropTypes.object,
@@ -18,7 +18,7 @@ function createMapStateToProps() {
(addMovie, importMovie, selectedIds) => { (addMovie, importMovie, selectedIds) => {
const { const {
monitor: defaultMonitor, monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId, qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability minimumAvailability: defaultMinimumAvailability
} = addMovie.defaults; } = addMovie.defaults;
@@ -30,7 +30,7 @@ function createMapStateToProps() {
} = importMovie; } = importMovie;
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor'); const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId'); const isQualityProfileIdsMixed = isMixed(items, selectedIds, defaultQualityProfileIds, 'qualityProfileIds');
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability'); const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated); const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
@@ -39,10 +39,10 @@ function createMapStateToProps() {
isLookingUpMovie, isLookingUpMovie,
isImporting, isImporting,
defaultMonitor, defaultMonitor,
defaultQualityProfileId, defaultQualityProfileIds,
defaultMinimumAvailability, defaultMinimumAvailability,
isMonitorMixed, isMonitorMixed,
isQualityProfileIdMixed, isQualityProfileIdsMixed,
isMinimumAvailabilityMixed, isMinimumAvailabilityMixed,
importError, importError,
hasUnsearchedItems hasUnsearchedItems
@@ -11,7 +11,7 @@ function ImportMovieRow(props) {
const { const {
id, id,
monitor, monitor,
qualityProfileId, qualityProfileIds,
minimumAvailability, minimumAvailability,
selectedMovie, selectedMovie,
isExistingMovie, isExistingMovie,
@@ -62,8 +62,8 @@ function ImportMovieRow(props) {
<VirtualTableRowCell className={styles.qualityProfile}> <VirtualTableRowCell className={styles.qualityProfile}>
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId" name="qualityProfileIds"
value={qualityProfileId} value={qualityProfileIds}
onChange={onInputChange} onChange={onInputChange}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
@@ -74,7 +74,7 @@ function ImportMovieRow(props) {
ImportMovieRow.propTypes = { ImportMovieRow.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired, monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired, qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
minimumAvailability: PropTypes.string.isRequired, minimumAvailability: PropTypes.string.isRequired,
selectedMovie: PropTypes.object, selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired, isExistingMovie: PropTypes.bool.isRequired,
@@ -15,7 +15,7 @@ class ImportMovieTable extends Component {
const { const {
unmappedFolders, unmappedFolders,
defaultMonitor, defaultMonitor,
defaultQualityProfileId, defaultQualityProfileIds,
defaultMinimumAvailability, defaultMinimumAvailability,
onMovieLookup, onMovieLookup,
onSetImportMovieValue onSetImportMovieValue
@@ -23,7 +23,7 @@ class ImportMovieTable extends Component {
const values = { const values = {
monitor: defaultMonitor, monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId, qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability minimumAvailability: defaultMinimumAvailability
}; };
@@ -167,7 +167,7 @@ ImportMovieTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object), items: PropTypes.arrayOf(PropTypes.object),
unmappedFolders: PropTypes.arrayOf(PropTypes.object), unmappedFolders: PropTypes.arrayOf(PropTypes.object),
defaultMonitor: PropTypes.string.isRequired, defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number, defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultMinimumAvailability: PropTypes.string, defaultMinimumAvailability: PropTypes.string,
allSelected: PropTypes.bool.isRequired, allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired, allUnselected: PropTypes.bool.isRequired,
@@ -13,7 +13,7 @@ function createMapStateToProps() {
(addMovie, importMovie, dimensions, allMovies) => { (addMovie, importMovie, dimensions, allMovies) => {
return { return {
defaultMonitor: addMovie.defaults.monitor, defaultMonitor: addMovie.defaults.monitor,
defaultQualityProfileId: addMovie.defaults.qualityProfileId, defaultQualityProfileIds: addMovie.defaults.qualityProfileIds,
defaultMinimumAvailability: addMovie.defaults.minimumAvailability, defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
items: importMovie.items, items: importMovie.items,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
@@ -5,6 +5,7 @@ import FormInputButton from 'Components/Form/FormInputButton';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal'; import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
@@ -242,7 +243,7 @@ class ImportMovieSelectMovie extends Component {
<FormInputButton <FormInputButton
kind={kinds.DEFAULT} kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH} spinnerIcon={icons.REFRESH}
canSpin={true} ButtonComponent={SpinnerButton}
isSpinning={isFetching} isSpinning={isFetching}
onPress={this.onRefreshPress} onPress={this.onRefreshPress}
> >
@@ -1,5 +1,4 @@
import SortDirection from 'Helpers/Props/SortDirection'; import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp } from './AppState';
export interface Error { export interface Error {
responseJSON: { responseJSON: {
@@ -21,10 +20,6 @@ export interface PagedAppSectionState {
pageSize: number; pageSize: number;
} }
export interface AppSectionFilterState<T> {
filterBuilderProps: FilterBuilderProp<T>[];
}
export interface AppSectionSchemaState<T> { export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean; isSchemaFetching: boolean;
isSchemaPopulated: boolean; isSchemaPopulated: boolean;
-2
View File
@@ -1,7 +1,6 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import CalendarAppState from './CalendarAppState'; import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState'; import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import MovieCollectionAppState from './MovieCollectionAppState'; import MovieCollectionAppState from './MovieCollectionAppState';
import MovieFilesAppState from './MovieFilesAppState'; import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState'; import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
@@ -47,7 +46,6 @@ export interface CustomFilter {
interface AppState { interface AppState {
calendar: CalendarAppState; calendar: CalendarAppState;
commands: CommandAppState; commands: CommandAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState; interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState; movieCollections: MovieCollectionAppState;
movieFiles: MovieFilesAppState; movieFiles: MovieFilesAppState;
+5 -6
View File
@@ -1,10 +1,9 @@
import AppSectionState, { import AppSectionState from 'App/State/AppSectionState';
AppSectionFilterState,
} from 'App/State/AppSectionState';
import Movie from 'Movie/Movie'; import Movie from 'Movie/Movie';
import { FilterBuilderProp } from './AppState';
interface CalendarAppState interface CalendarAppState extends AppSectionState<Movie> {
extends AppSectionState<Movie>, filterBuilderProps: FilterBuilderProp<Movie>[];
AppSectionFilterState<Movie> {} }
export default CalendarAppState; export default CalendarAppState;
-10
View File
@@ -1,10 +0,0 @@
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import History from 'typings/History';
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History> {}
export default HistoryAppState;
@@ -1,8 +1,6 @@
import AppSectionState from 'App/State/AppSectionState'; import AppSectionState from 'App/State/AppSectionState';
import MovieCollection from 'typings/MovieCollection'; import MovieCollection from 'typings/MovieCollection';
interface MovieCollectionAppState extends AppSectionState<MovieCollection> { type MovieCollectionAppState = AppSectionState<MovieCollection>;
itemMap: Record<number, number>;
}
export default MovieCollectionAppState; export default MovieCollectionAppState;
+2 -8
View File
@@ -2,11 +2,7 @@ import ModelBase from 'App/ModelBase';
import Language from 'Language/Language'; import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality'; import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat'; import CustomFormat from 'typings/CustomFormat';
import AppSectionState, { import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
AppSectionFilterState,
AppSectionItemState,
Error,
} from './AppSectionState';
export interface StatusMessage { export interface StatusMessage {
title: string; title: string;
@@ -39,9 +35,7 @@ export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown; params: unknown;
} }
export interface QueuePagedAppState export interface QueuePagedAppState extends AppSectionState<Queue> {
extends AppSectionState<Queue>,
AppSectionFilterState<Queue> {
isGrabbing: boolean; isGrabbing: boolean;
grabError: Error; grabError: Error;
isRemoving: boolean; isRemoving: boolean;
+2 -2
View File
@@ -42,9 +42,9 @@ function Agenda(props) {
<div className={styles.agenda}> <div className={styles.agenda}>
{ {
items.map((item, index) => { items.map((item, index) => {
const momentDate = moment(item.sortDate); const momentDate = moment(item.inCinemas);
const showDate = index === 0 || const showDate = index === 0 ||
!moment(items[index - 1].sortDate).isSame(momentDate, 'day'); !moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
return ( return (
<AgendaEventConnector <AgendaEventConnector
+2 -1
View File
@@ -88,7 +88,7 @@
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {
.overlay { .event {
flex-direction: column; flex-direction: column;
} }
@@ -111,4 +111,5 @@
.releaseIcon { .releaseIcon {
margin-right: 20px; margin-right: 20px;
width: 25px; width: 25px;
text-align: right;
} }
+1 -1
View File
@@ -95,7 +95,7 @@ class AgendaEvent extends Component {
<div className={styles.overlay}> <div className={styles.overlay}>
<div className={styles.date}> <div className={styles.date}>
{showDate ? startTime.format(longDateFormat) : null} {(showDate) ? startTime.format(longDateFormat) : null}
</div> </div>
<div className={styles.releaseIcon}> <div className={styles.releaseIcon}>
@@ -23,11 +23,13 @@ function createFilterBuilderPropsSelector() {
); );
} }
interface CalendarFilterModalProps { interface SeriesIndexFilterModalProps {
isOpen: boolean; isOpen: boolean;
} }
export default function CalendarFilterModal(props: CalendarFilterModalProps) { export default function CalendarFilterModal(
props: SeriesIndexFilterModalProps
) {
const sectionItems = useSelector(createCalendarSelector()); const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar'; const customFilterType = 'calendar';
@@ -25,7 +25,7 @@ function createMissingMovieIdsSelector() {
const inCinemas = movie.inCinemas; const inCinemas = movie.inCinemas;
if ( if (
!movie.hasFile && (!movie.statistics || movie.statistics.movieFileCount === 0) &&
moment(inCinemas).isAfter(start) && moment(inCinemas).isAfter(start) &&
moment(inCinemas).isBefore(end) && moment(inCinemas).isBefore(end) &&
isBefore(movie.inCinemas) && isBefore(movie.inCinemas) &&
@@ -46,7 +46,7 @@ class AddNewCollectionMovieModalContent extends Component {
onInputChange, onInputChange,
rootFolderPath, rootFolderPath,
monitor, monitor,
qualityProfileId, qualityProfileIds,
minimumAvailability, minimumAvailability,
searchForMovie searchForMovie
} = this.props; } = this.props;
@@ -126,13 +126,13 @@ class AddNewCollectionMovieModalContent extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel> <FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId" name="qualityProfileIds"
onChange={this.onQualityProfileIdChange} onChange={this.onQualityProfileIdChange}
{...qualityProfileId} {...qualityProfileIds}
/> />
</FormGroup> </FormGroup>
@@ -189,7 +189,7 @@ AddNewCollectionMovieModalContent.propTypes = {
addError: PropTypes.object, addError: PropTypes.object,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileIds: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired, searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired, folder: PropTypes.string.isRequired,
@@ -25,7 +25,7 @@ function createMapStateToProps() {
const collectionDefaults = { const collectionDefaults = {
rootFolderPath: collection.rootFolderPath, rootFolderPath: collection.rootFolderPath,
monitor: 'movieOnly', monitor: 'movieOnly',
qualityProfileId: collection.qualityProfileId, qualityProfileIds: collection.qualityProfileIds,
minimumAvailability: collection.minimumAvailability, minimumAvailability: collection.minimumAvailability,
searchForMovie: collection.searchOnAdd, searchForMovie: collection.searchOnAdd,
tags: collection.tags || [] tags: collection.tags || []
@@ -70,7 +70,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title, title,
rootFolderPath, rootFolderPath,
monitor, monitor,
qualityProfileId, qualityProfileIds,
minimumAvailability, minimumAvailability,
searchForMovie, searchForMovie,
tags tags
@@ -81,7 +81,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title, title,
rootFolderPath: rootFolderPath.value, rootFolderPath: rootFolderPath.value,
monitor: monitor.value, monitor: monitor.value,
qualityProfileId: qualityProfileId.value, qualityProfileIds: qualityProfileIds.value,
minimumAvailability: minimumAvailability.value, minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value, searchForMovie: searchForMovie.value,
tags: tags.value tags: tags.value
@@ -109,7 +109,7 @@ AddNewCollectionMovieModalContentConnector.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileIds: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired, searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,
@@ -21,7 +21,6 @@ function createMapStateToProps() {
return { return {
...collection, ...collection,
movies: [...collection.movies].sort((a, b) => b.year - a.year),
genres: Array.from(new Set(allGenres)).slice(0, 3) genres: Array.from(new Set(allGenres)).slice(0, 3)
}; };
} }
@@ -46,7 +46,7 @@ class EditCollectionModalContent extends Component {
const { const {
monitored, monitored,
qualityProfileId, qualityProfileIds,
minimumAvailability, minimumAvailability,
// Id, // Id,
rootFolderPath, rootFolderPath,
@@ -105,12 +105,12 @@ class EditCollectionModalContent extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel> <FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId" name="qualityProfileIds"
{...qualityProfileId} {...qualityProfileIds}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
@@ -39,7 +39,7 @@ function createMapStateToProps() {
const movieSettings = { const movieSettings = {
monitored: collection.monitored, monitored: collection.monitored,
qualityProfileId: collection.qualityProfileId, qualityProfileIds: collection.qualityProfileIds,
minimumAvailability: collection.minimumAvailability, minimumAvailability: collection.minimumAvailability,
rootFolderPath: collection.rootFolderPath, rootFolderPath: collection.rootFolderPath,
tags: collection.tags, tags: collection.tags,
@@ -61,7 +61,6 @@ class CollectionMovie extends Component {
const { const {
id, id,
title, title,
status,
overview, overview,
year, year,
tmdbId, tmdbId,
@@ -124,11 +123,11 @@ class CollectionMovie extends Component {
<div className={styles.overlay}> <div className={styles.overlay}>
<div className={styles.overlayTitle}> <div className={styles.overlayTitle}>
{title} {year > 0 ? `(${year})` : ''} {title}
</div> </div>
{ {
id ? id &&
<div className={styles.overlayStatus}> <div className={styles.overlayStatus}>
<MovieIndexProgressBar <MovieIndexProgressBar
monitored={monitored} monitored={monitored}
@@ -139,8 +138,7 @@ class CollectionMovie extends Component {
detailedProgressBar={detailedProgressBar} detailedProgressBar={detailedProgressBar}
isAvailable={isAvailable} isAvailable={isAvailable}
/> />
</div> : </div>
null
} }
</div> </div>
</Link> </Link>
@@ -173,7 +171,6 @@ CollectionMovie.propTypes = {
id: PropTypes.number, id: PropTypes.number,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired, overview: PropTypes.string.isRequired,
monitored: PropTypes.bool, monitored: PropTypes.bool,
collectionId: PropTypes.number.isRequired, collectionId: PropTypes.number.isRequired,
@@ -5,7 +5,7 @@
margin: 2px 4px; margin: 2px 4px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
background-color: var(--inputBackgroundColor); background-color: #eee;
cursor: default; cursor: default;
} }
@@ -17,7 +17,7 @@
padding: 0 4px; padding: 0 4px;
border-left: 4px; border-left: 4px;
border-left-style: solid; border-left-style: solid;
background-color: var(--themeLightColor); background-color: var(--white);
color: var(--defaultColor); color: var(--defaultColor);
} }
@@ -14,15 +14,16 @@ class CollectionMovieLabel extends Component {
const { const {
id, id,
title, title,
year,
status, status,
monitored, monitored,
isAvailable, isAvailable,
hasFile,
onMonitorTogglePress, onMonitorTogglePress,
isSaving isSaving,
statistics
} = this.props; } = this.props;
const { movieFileCount } = statistics;
return ( return (
<div className={styles.movie}> <div className={styles.movie}>
<div className={styles.movieTitle}> <div className={styles.movieTitle}>
@@ -36,7 +37,9 @@ class CollectionMovieLabel extends Component {
} }
<span> <span>
{title} {year > 0 ? `(${year})` : ''} {
title
}
</span> </span>
</div> </div>
@@ -45,11 +48,11 @@ class CollectionMovieLabel extends Component {
<div <div
className={classNames( className={classNames(
styles.movieStatus, styles.movieStatus,
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')] styles[getStatusStyle(status, monitored, movieFileCount > 0, isAvailable, 'kinds')]
)} )}
> >
{ {
hasFile ? translate('Downloaded') : translate('Missing') movieFileCount > 0 ? translate('Downloaded') : translate('Missing')
} }
</div> </div>
} }
@@ -61,11 +64,10 @@ class CollectionMovieLabel extends Component {
CollectionMovieLabel.propTypes = { CollectionMovieLabel.propTypes = {
id: PropTypes.number, id: PropTypes.number,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
status: PropTypes.string, status: PropTypes.string,
statistics: PropTypes.object.isRequired,
isAvailable: PropTypes.bool, isAvailable: PropTypes.bool,
monitored: PropTypes.bool, monitored: PropTypes.bool,
hasFile: PropTypes.bool,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
movieFile: PropTypes.object, movieFile: PropTypes.object,
movieFileId: PropTypes.number, movieFileId: PropTypes.number,
@@ -75,9 +77,7 @@ CollectionMovieLabel.propTypes = {
CollectionMovieLabel.defaultProps = { CollectionMovieLabel.defaultProps = {
isSaving: false, isSaving: false,
statistics: { statistics: {
episodeFileCount: 0, movieFileCount: 0
totalEpisodeCount: 0,
percentOfEpisodes: 0
} }
}; };
@@ -96,7 +96,7 @@ class CollectionOverview extends Component {
render() { render() {
const { const {
monitored, monitored,
qualityProfileId, qualityProfileIds,
rootFolderPath, rootFolderPath,
genres, genres,
id, id,
@@ -212,7 +212,7 @@ class CollectionOverview extends Component {
<span className={styles.qualityProfileName}> <span className={styles.qualityProfileName}>
{ {
<QualityProfileNameConnector <QualityProfileNameConnector
qualityProfileId={qualityProfileId} qualityProfileIds={qualityProfileIds}
/> />
} }
</span> </span>
@@ -325,7 +325,7 @@ class CollectionOverview extends Component {
CollectionOverview.propTypes = { CollectionOverview.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
qualityProfileId: PropTypes.number.isRequired, qualityProfileIds: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired, minimumAvailability: PropTypes.string.isRequired,
searchOnAdd: PropTypes.bool.isRequired, searchOnAdd: PropTypes.bool.isRequired,
rootFolderPath: PropTypes.string.isRequired, rootFolderPath: PropTypes.string.isRequired,
@@ -28,6 +28,7 @@ function calculatePosterWidth(posterSize, isSmallScreen) {
} }
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) { function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
const heights = [ const heights = [
overviewOptions.showPosters ? posterHeight : 75, overviewOptions.showPosters ? posterHeight : 75,
isSmallScreen ? columnPaddingSmallScreen : columnPadding isSmallScreen ? columnPaddingSmallScreen : columnPadding
@@ -121,8 +122,8 @@ class CollectionOverviews extends Component {
overviewOptions overviewOptions
} = this.props; } = this.props;
const posterWidth = overviewOptions.showPosters ? calculatePosterWidth(overviewOptions.size, isSmallScreen) : 0; const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
const posterHeight = overviewOptions.showPosters ? calculatePosterHeight(posterWidth) : 0; const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions); const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
this.setState({ this.setState({
@@ -6,12 +6,9 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector'; import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue'; import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
@@ -61,15 +58,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.DATE: case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue; return DateFilterBuilderRowValue;
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
return HistoryEventTypeFilterBuilderRowValue;
case filterBuilderValueTypes.INDEXER: case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector; return IndexerFilterBuilderRowValueConnector;
case filterBuilderValueTypes.LANGUAGE:
return LanguageFilterBuilderRowValue;
case filterBuilderValueTypes.PROTOCOL: case filterBuilderValueTypes.PROTOCOL:
return ProtocolFilterBuilderRowValue; return ProtocolFilterBuilderRowValue;
@@ -79,9 +70,6 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.QUALITY_PROFILE: case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector; return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.MOVIE:
return MovieFilterBuilderRowValue;
case filterBuilderValueTypes.RELEASE_STATUS: case filterBuilderValueTypes.RELEASE_STATUS:
return ReleaseStatusFilterBuilderRowValue; return ReleaseStatusFilterBuilderRowValue;
@@ -1,16 +0,0 @@
import { FilterBuilderProp } from 'App/State/AppState';
interface FilterBuilderRowOnChangeProps {
name: string;
value: unknown[];
}
interface FilterBuilderRowValueProps {
filterType?: string;
filterValue: string | number | object | string[] | number[] | object[];
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
sectionItem: unknown[];
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
}
export default FilterBuilderRowValueProps;
@@ -1,5 +1,5 @@
.tag { .tag {
display: flex; height: 21px;
&.isLastTag { &.isLastTag {
.or { .or {
@@ -18,5 +18,4 @@
.or { .or {
margin: 0 3px; margin: 0 3px;
color: var(--themeDarkColor); color: var(--themeDarkColor);
line-height: 31px;
} }
@@ -7,7 +7,7 @@ import styles from './FilterBuilderRowValueTag.css';
function FilterBuilderRowValueTag(props) { function FilterBuilderRowValueTag(props) {
return ( return (
<div <span
className={styles.tag} className={styles.tag}
> >
<TagInputTag <TagInputTag
@@ -22,7 +22,7 @@ function FilterBuilderRowValueTag(props) {
{translate('Or')} {translate('Or')}
</div> </div>
} }
</div> </span>
); );
} }
@@ -1,51 +0,0 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
const EVENT_TYPE_OPTIONS = [
{
id: 1,
get name() {
return translate('Grabbed');
},
},
{
id: 3,
get name() {
return translate('Imported');
},
},
{
id: 4,
get name() {
return translate('Failed');
},
},
{
id: 5,
get name() {
return translate('Deleted');
},
},
{
id: 6,
get name() {
return translate('Renamed');
},
},
{
id: 7,
get name() {
return translate('Ignored');
},
},
];
function HistoryEventTypeFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
}
export default HistoryEventTypeFilterBuilderRowValue;
@@ -1,13 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const { items } = useSelector(createLanguagesSelector());
return <FilterBuilderRowValue {...props} tagList={items} />;
}
export default LanguageFilterBuilderRowValue;
@@ -1,19 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Movie from 'Movie/Movie';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import sortByName from 'Utilities/Array/sortByName';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const allMovies: Movie[] = useSelector(createAllMoviesSelector());
const tagList = allMovies
.map((movie) => ({ id: movie.id, name: movie.title }))
.sort(sortByName);
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}
export default MovieFilterBuilderRowValue;
@@ -2,33 +2,19 @@ import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import styles from './FormInputButton.css'; import styles from './FormInputButton.css';
function FormInputButton(props) { function FormInputButton(props) {
const { const {
className, className,
canSpin, ButtonComponent,
isLastButton, isLastButton,
...otherProps ...otherProps
} = props; } = props;
if (canSpin) {
return (
<SpinnerButton
className={classNames(
className,
!isLastButton && styles.middleButton
)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
return ( return (
<Button <ButtonComponent
className={classNames( className={classNames(
className, className,
!isLastButton && styles.middleButton !isLastButton && styles.middleButton
@@ -41,14 +27,14 @@ function FormInputButton(props) {
FormInputButton.propTypes = { FormInputButton.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
isLastButton: PropTypes.bool.isRequired, ButtonComponent: PropTypes.elementType.isRequired,
canSpin: PropTypes.bool.isRequired isLastButton: PropTypes.bool.isRequired
}; };
FormInputButton.defaultProps = { FormInputButton.defaultProps = {
className: styles.button, className: styles.button,
isLastButton: true, ButtonComponent: Button,
canSpin: false isLastButton: true
}; };
export default FormInputButton; export default FormInputButton;
@@ -6,7 +6,6 @@
.inputGroup { .inputGroup {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-wrap: wrap;
} }
.inputContainer { .inputContainer {
@@ -21,6 +21,7 @@ import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector'; import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector'; import PathInputConnector from './PathInputConnector';
import PlexMachineInputConnector from './PlexMachineInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import TagInputConnector from './TagInputConnector'; import TagInputConnector from './TagInputConnector';
@@ -63,6 +64,9 @@ function getComponent(type) {
case inputTypes.PATH: case inputTypes.PATH:
return PathInputConnector; return PathInputConnector;
case inputTypes.PLEX_MACHINE_SELECT:
return PlexMachineInputConnector;
case inputTypes.QUALITY_PROFILE_SELECT: case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector; return QualityProfileSelectInputConnector;
+1 -3
View File
@@ -2,10 +2,8 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-right: $formLabelRightMarginWidth; margin-right: $formLabelRightMarginWidth;
padding-top: 8px;
min-height: 35px;
text-align: end;
font-weight: bold; font-weight: bold;
line-height: 35px;
} }
.hasError { .hasError {
+12 -11
View File
@@ -5,6 +5,7 @@ import { kinds } from 'Helpers/Props';
function OAuthInput(props) { function OAuthInput(props) {
const { const {
className,
label, label,
authorizing, authorizing,
error, error,
@@ -12,21 +13,21 @@ function OAuthInput(props) {
} = props; } = props;
return ( return (
<div> <SpinnerErrorButton
<SpinnerErrorButton className={className}
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
isSpinning={authorizing} isSpinning={authorizing}
error={error} error={error}
onPress={onPress} onPress={onPress}
> >
{label} {label}
</SpinnerErrorButton> </SpinnerErrorButton>
</div>
); );
} }
OAuthInput.propTypes = { OAuthInput.propTypes = {
label: PropTypes.string.isRequired, className: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
authorizing: PropTypes.bool.isRequired, authorizing: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
onPress: PropTypes.func.isRequired onPress: PropTypes.func.isRequired
@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectInput from './SelectInput';
function PlexMachineInput(props) {
const {
isFetching,
isDisabled,
value,
values,
onChange,
...otherProps
} = props;
const helpText = 'Authenticate with plex.tv to show servers to use for authentication';
return (
<>
{
isFetching ?
<LoadingIndicator /> :
<SelectInput
value={value}
values={values}
isDisabled={isDisabled}
onChange={onChange}
helpText={helpText}
{...otherProps}
/>
}
</>
);
}
PlexMachineInput.propTypes = {
isFetching: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
value: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default PlexMachineInput;
@@ -0,0 +1,115 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchPlexResources } from 'Store/Actions/settingsActions';
import PlexMachineInput from './PlexMachineInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.oAuth,
(state) => state.settings.plex,
(value, oAuth, plex) => {
let values = [{ key: value, value }];
let isDisabled = true;
if (plex.isPopulated) {
const serverValues = plex.items.filter((item) => item.provides.includes('server')).map((item) => {
return ({
key: item.clientIdentifier,
value: `${item.name} / ${item.owned ? 'Owner' : 'User'} / ${item.clientIdentifier}`
});
});
if (serverValues.find((item) => item.key === value)) {
values = serverValues;
} else {
values = values.concat(serverValues);
}
isDisabled = false;
}
return ({
accessToken: oAuth.result?.accessToken,
values,
isDisabled,
...plex
});
}
);
}
const mapDispatchToProps = {
dispatchFetchPlexResources: fetchPlexResources
};
class PlexMachineInputConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
const {
accessToken,
dispatchFetchPlexResources
} = this.props;
if (accessToken) {
dispatchFetchPlexResources({ accessToken });
}
};
componentDidUpdate(prevProps) {
const {
accessToken,
dispatchFetchPlexResources
} = this.props;
const oldToken = prevProps.accessToken;
if (accessToken && accessToken !== oldToken) {
dispatchFetchPlexResources({ accessToken });
}
}
render() {
const {
isFetching,
isPopulated,
isDisabled,
value,
values,
onChange
} = this.props;
return (
<PlexMachineInput
isFetching={isFetching}
isPopulated={isPopulated}
isDisabled={isDisabled}
value={value}
values={values}
onChange={onChange}
{...this.props}
/>
);
}
}
PlexMachineInputConnector.propTypes = {
dispatchFetchPlexResources: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
error: PropTypes.object,
oAuth: PropTypes.object,
accessToken: PropTypes.string,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PlexMachineInputConnector);
@@ -37,8 +37,6 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.OAUTH; return inputTypes.OAUTH;
case 'rootFolder': case 'rootFolder':
return inputTypes.ROOT_FOLDER_SELECT; return inputTypes.ROOT_FOLDER_SELECT;
case 'qualityProfile':
return inputTypes.QUALITY_PROFILE_SELECT;
default: default:
return inputTypes.TEXT; return inputTypes.TEXT;
} }
@@ -47,32 +47,6 @@ function createMapStateToProps() {
class QualityProfileSelectInputConnector extends Component { class QualityProfileSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
name,
value,
values
} = this.props;
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
if (firstValue) {
this.onChange({ name, value: firstValue.key });
}
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
};
// //
// Render // Render
@@ -80,7 +54,7 @@ class QualityProfileSelectInputConnector extends Component {
return ( return (
<EnhancedSelectInput <EnhancedSelectInput
{...this.props} {...this.props}
onChange={this.onChange} onChange={this.props.onChange}
/> />
); );
} }
@@ -88,7 +62,7 @@ class QualityProfileSelectInputConnector extends Component {
QualityProfileSelectInputConnector.propTypes = { QualityProfileSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string)]),
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeNoChange: PropTypes.bool.isRequired, includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired
@@ -12,6 +12,10 @@
composes: hasWarning from '~Components/Form/Input.css'; composes: hasWarning from '~Components/Form/Input.css';
} }
.hasButton {
composes: hasButton from '~Components/Form/Input.css';
}
.isDisabled { .isDisabled {
opacity: 0.7; opacity: 0.7;
cursor: not-allowed; cursor: not-allowed;
+1
View File
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'hasButton': string;
'hasError': string; 'hasError': string;
'hasWarning': string; 'hasWarning': string;
'isDisabled': string; 'isDisabled': string;
@@ -28,6 +28,7 @@ class SelectInput extends Component {
isDisabled, isDisabled,
hasError, hasError,
hasWarning, hasWarning,
hasButton,
autoFocus, autoFocus,
onBlur onBlur
} = this.props; } = this.props;
@@ -38,6 +39,7 @@ class SelectInput extends Component {
className, className,
hasError && styles.hasError, hasError && styles.hasError,
hasWarning && styles.hasWarning, hasWarning && styles.hasWarning,
hasButton && styles.hasButton,
isDisabled && disabledClassName isDisabled && disabledClassName
)} )}
disabled={isDisabled} disabled={isDisabled}
@@ -80,6 +82,7 @@ SelectInput.propTypes = {
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
hasError: PropTypes.bool, hasError: PropTypes.bool,
hasWarning: PropTypes.bool, hasWarning: PropTypes.bool,
hasButton: PropTypes.bool,
autoFocus: PropTypes.bool.isRequired, autoFocus: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func onBlur: PropTypes.func
@@ -3,7 +3,6 @@ import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ModalContent.css'; import styles from './ModalContent.css';
function ModalContent(props) { function ModalContent(props) {
@@ -29,7 +28,6 @@ function ModalContent(props) {
<Icon <Icon
name={icons.CLOSE} name={icons.CLOSE}
size={18} size={18}
title={translate('Close')}
/> />
</Link> </Link>
} }
@@ -82,7 +82,6 @@ class PageHeader extends Component {
aria-label="Donate" aria-label="Donate"
to="https://radarr.video/donate" to="https://radarr.video/donate"
size={14} size={14}
title={translate('Donate')}
/> />
<IconButton <IconButton
className={styles.translate} className={styles.translate}
@@ -12,7 +12,7 @@ import styles from './PageHeaderActionsMenu.css';
function PageHeaderActionsMenu(props) { function PageHeaderActionsMenu(props) {
const { const {
formsAuth, cookieAuth,
onKeyboardShortcutsPress, onKeyboardShortcutsPress,
onRestartPress, onRestartPress,
onShutdownPress onShutdownPress
@@ -24,7 +24,6 @@ function PageHeaderActionsMenu(props) {
<MenuButton className={styles.menuButton} aria-label="Menu Button"> <MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon <Icon
name={icons.INTERACTIVE} name={icons.INTERACTIVE}
title={translate('Menu')}
/> />
</MenuButton> </MenuButton>
@@ -57,22 +56,20 @@ function PageHeaderActionsMenu(props) {
</MenuItem> </MenuItem>
{ {
formsAuth && cookieAuth &&
<div className={styles.separator} /> <>
} <div className={styles.separator} />
<MenuItem
{ to={`${window.Radarr.urlBase}/logout?ReturnUrl=/`}
formsAuth && noRouter={true}
<MenuItem >
to={`${window.Radarr.urlBase}/logout`} <Icon
noRouter={true} className={styles.itemIcon}
> name={icons.LOGOUT}
<Icon />
className={styles.itemIcon} Logout
name={icons.LOGOUT} </MenuItem>
/> </>
Logout
</MenuItem>
} }
</MenuContent> </MenuContent>
</Menu> </Menu>
@@ -81,7 +78,7 @@ function PageHeaderActionsMenu(props) {
} }
PageHeaderActionsMenu.propTypes = { PageHeaderActionsMenu.propTypes = {
formsAuth: PropTypes.bool.isRequired, cookieAuth: PropTypes.bool.isRequired,
onKeyboardShortcutsPress: PropTypes.func.isRequired, onKeyboardShortcutsPress: PropTypes.func.isRequired,
onRestartPress: PropTypes.func.isRequired, onRestartPress: PropTypes.func.isRequired,
onShutdownPress: PropTypes.func.isRequired onShutdownPress: PropTypes.func.isRequired
@@ -10,7 +10,7 @@ function createMapStateToProps() {
(state) => state.system.status, (state) => state.system.status,
(status) => { (status) => {
return { return {
formsAuth: status.item.authentication === 'forms' cookieAuth: ['forms', 'oidc', 'plex'].includes(status.item.authentication)
}; };
} }
); );
@@ -0,0 +1,3 @@
.tags {
flex: 1 0 auto;
}
+7
View File
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,46 @@
import _ from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { kinds } from 'Helpers/Props';
import Label from './Label';
import styles from './QualityProfileList.css';
interface QualityProfileListProps {
qualityProfileIds: number[];
}
function QualityProfileList(props: QualityProfileListProps) {
const { qualityProfileIds } = props;
const { qualityProfileList } = useSelector(
createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfileList) => {
return {
qualityProfileList,
};
}
)
);
return (
<div className={styles.tags}>
{qualityProfileIds.map((t) => {
const qualityProfile = _.find(qualityProfileList, { id: t });
if (!qualityProfile) {
return null;
}
return (
<Label key={qualityProfile.id} kind={kinds.INFO}>
{qualityProfile.name}
</Label>
);
})}
</div>
);
}
export default QualityProfileList;
@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import QualityProfileList from './QualityProfileList';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles.items,
(qualityProfileList) => {
return {
qualityProfileList
};
}
);
}
export default connect(createMapStateToProps)(QualityProfileList);
@@ -2,19 +2,38 @@ import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import OAuthInputConnector from 'Components/Form/OAuthInputConnector';
import Icon from 'Components/Icon';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { icons, inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings'; import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css'; import styles from './AuthenticationRequiredModalContent.css';
const oauthData = {
implementation: { value: 'PlexImport' },
configContract: { value: 'PlexListSettings' },
fields: [
{
type: 'textbox',
name: 'accessToken'
},
{
type: 'oAuth',
name: 'signIn',
value: 'startAuth'
}
]
};
function onModalClose() { function onModalClose() {
// No-op // No-op
} }
@@ -22,6 +41,7 @@ function onModalClose() {
function AuthenticationRequiredModalContent(props) { function AuthenticationRequiredModalContent(props) {
const { const {
isPopulated, isPopulated,
plexServersPopulated,
error, error,
isSaving, isSaving,
settings, settings,
@@ -34,10 +54,18 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod, authenticationMethod,
authenticationRequired, authenticationRequired,
username, username,
password password,
plexAuthServer,
plexRequireOwner,
oidcClientId,
oidcClientSecret,
oidcAuthority
} = settings; } = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const showUserPass = authenticationMethod && ['basic', 'forms'].includes(authenticationMethod.value);
const plexEnabled = authenticationMethod && authenticationMethod.value === 'plex';
const oidcEnabled = authenticationMethod && authenticationMethod.value === 'oidc';
const didMount = useRef(false); const didMount = useRef(false);
@@ -97,29 +125,111 @@ function AuthenticationRequiredModalContent(props) {
/> />
</FormGroup> </FormGroup>
<FormGroup> {
<FormLabel>{translate('Username')}</FormLabel> showUserPass &&
<>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="username" name="username"
onChange={onInputChange} onChange={onInputChange}
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')} helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
{...username} {...username}
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('Password')}</FormLabel> <FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.PASSWORD} type={inputTypes.PASSWORD}
name="password" name="password"
onChange={onInputChange} onChange={onInputChange}
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')} helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
{...password} {...password}
/> />
</FormGroup> </FormGroup>
</>
}
{
plexEnabled &&
<>
<FormGroup>
<FormLabel>Plex Server</FormLabel>
<FormInputGroup
type={inputTypes.PLEX_MACHINE_SELECT}
name="plexAuthServer"
buttons={[
<FormInputButton
key="auth"
ButtonComponent={OAuthInputConnector}
label={plexServersPopulated ? <Icon name={icons.REFRESH} /> : 'Fetch'}
name="plexAuth"
provider="importList"
providerData={oauthData}
section="settings.importLists"
onChange={onInputChange}
/>
]}
onChange={onInputChange}
{...plexAuthServer}
/>
</FormGroup>
<FormGroup>
<FormLabel>Restrict Access to Server Owner</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="plexRequireOwner"
onChange={onInputChange}
{...plexRequireOwner}
/>
</FormGroup>
</>
}
{
oidcEnabled &&
<>
<FormGroup>
<FormLabel>Authority</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcAuthority"
onChange={onInputChange}
{...oidcAuthority}
/>
</FormGroup>
<FormGroup>
<FormLabel>ClientId</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcClientId"
onChange={onInputChange}
{...oidcClientId}
/>
</FormGroup>
<FormGroup>
<FormLabel>ClientSecret</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="oidcClientSecret"
onChange={onInputChange}
{...oidcClientSecret}
/>
</FormGroup>
</>
}
</div> : </div> :
null null
} }
@@ -145,6 +255,7 @@ function AuthenticationRequiredModalContent(props) {
AuthenticationRequiredModalContent.propTypes = { AuthenticationRequiredModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
plexServersPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
@@ -13,9 +13,11 @@ const SECTION = 'general';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSettingsSectionSelector(SECTION), createSettingsSectionSelector(SECTION),
(sectionSettings) => { (state) => state.settings.plex,
(sectionSettings, plex) => {
return { return {
...sectionSettings ...sectionSettings,
plexServersPopulated: plex.isPopulated
}; };
} }
); );
@@ -2,13 +2,10 @@ export const BOOL = 'bool';
export const BYTES = 'bytes'; export const BYTES = 'bytes';
export const DATE = 'date'; export const DATE = 'date';
export const DEFAULT = 'default'; export const DEFAULT = 'default';
export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer'; export const INDEXER = 'indexer';
export const LANGUAGE = 'language';
export const PROTOCOL = 'protocol'; export const PROTOCOL = 'protocol';
export const QUALITY = 'quality'; export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile'; export const QUALITY_PROFILE = 'qualityProfile';
export const MOVIE = 'movie';
export const RELEASE_STATUS = 'releaseStatus'; export const RELEASE_STATUS = 'releaseStatus';
export const MINIMUM_AVAILABILITY = 'minimumAvailability'; export const MINIMUM_AVAILABILITY = 'minimumAvailability';
export const TAG = 'tag'; export const TAG = 'tag';
+2
View File
@@ -10,6 +10,7 @@ export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
export const PATH = 'path'; export const PATH = 'path';
export const PLEX_MACHINE_SELECT = 'plexMachineSelect';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect'; export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
@@ -38,6 +39,7 @@ export const all = [
OAUTH, OAUTH,
PASSWORD, PASSWORD,
PATH, PATH,
PLEX_MACHINE_SELECT,
QUALITY_PROFILE_SELECT, QUALITY_PROFILE_SELECT,
INDEXER_SELECT, INDEXER_SELECT,
DOWNLOAD_CLIENT_SELECT, DOWNLOAD_CLIENT_SELECT,
@@ -49,20 +49,24 @@ class DeleteMovieModalContent extends Component {
const { const {
title, title,
path, path,
hasFile, statistics,
deleteOptions, deleteOptions,
sizeOnDisk,
onModalClose, onModalClose,
onDeleteOptionChange onDeleteOptionChange
} = this.props; } = this.props;
const {
sizeOnDisk,
movieFileCount
} = statistics;
const deleteFiles = this.state.deleteFiles; const deleteFiles = this.state.deleteFiles;
const addImportExclusion = deleteOptions.addImportExclusion; const addImportExclusion = deleteOptions.addImportExclusion;
let deleteFilesLabel = hasFile ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [0]); let deleteFilesLabel = movieFileCount === 1 ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [movieFileCount]);
let deleteFilesHelpText = translate('DeleteFilesHelpText'); let deleteFilesHelpText = translate('DeleteFilesHelpText');
if (!hasFile) { if (movieFileCount === 0) {
deleteFilesLabel = translate('DeleteMovieFolderLabel'); deleteFilesLabel = translate('DeleteMovieFolderLabel');
deleteFilesHelpText = translate('DeleteMovieFolderHelpText'); deleteFilesHelpText = translate('DeleteMovieFolderHelpText');
} }
@@ -121,9 +125,9 @@ class DeleteMovieModalContent extends Component {
</div> </div>
{ {
!!hasFile && movieFileCount > 0 &&
<div> <div>
{hasFile} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)} {movieFileCount} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
</div> </div>
} }
</div> </div>
@@ -151,12 +155,18 @@ class DeleteMovieModalContent extends Component {
DeleteMovieModalContent.propTypes = { DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
hasFile: PropTypes.bool.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
deleteOptions: PropTypes.object.isRequired, deleteOptions: PropTypes.object.isRequired,
onDeleteOptionChange: PropTypes.func.isRequired, onDeleteOptionChange: PropTypes.func.isRequired,
statistics: PropTypes.object.isRequired,
onDeletePress: PropTypes.func.isRequired, onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
DeleteMovieModalContent.defaultProps = {
statistics: {
sizeOnDisk: 0,
movieFileCount: 0
}
};
export default DeleteMovieModalContent; export default DeleteMovieModalContent;
@@ -202,12 +202,6 @@
.headerContent { .headerContent {
padding: 15px; padding: 15px;
} }
.title {
font-weight: 300;
font-size: 30px;
line-height: 30px;
}
} }
@media only screen and (max-width: $breakpointLarge) { @media only screen and (max-width: $breakpointLarge) {
+5 -5
View File
@@ -44,7 +44,7 @@ import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector'; import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector'; import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
import MovieDetailsLinks from './MovieDetailsLinks'; import MovieDetailsLinks from './MovieDetailsLinks';
import MovieReleaseDates from './MovieReleaseDates'; import MovieReleaseDatesConnector from './MovieReleaseDatesConnector';
import MovieStatusLabel from './MovieStatusLabel'; import MovieStatusLabel from './MovieStatusLabel';
import MovieTagsConnector from './MovieTagsConnector'; import MovieTagsConnector from './MovieTagsConnector';
import MovieTitlesTable from './Titles/MovieTitlesTable'; import MovieTitlesTable from './Titles/MovieTitlesTable';
@@ -262,7 +262,7 @@ class MovieDetails extends Component {
ratings, ratings,
path, path,
sizeOnDisk, sizeOnDisk,
qualityProfileId, qualityProfileIds,
monitored, monitored,
studio, studio,
genres, genres,
@@ -433,7 +433,7 @@ class MovieDetails extends Component {
} }
title={translate('ReleaseDates')} title={translate('ReleaseDates')}
body={ body={
<MovieReleaseDates <MovieReleaseDatesConnector
inCinemas={inCinemas} inCinemas={inCinemas}
physicalRelease={physicalRelease} physicalRelease={physicalRelease}
digitalRelease={digitalRelease} digitalRelease={digitalRelease}
@@ -557,7 +557,7 @@ class MovieDetails extends Component {
<span className={styles.qualityProfileName}> <span className={styles.qualityProfileName}>
{ {
<QualityProfileNameConnector <QualityProfileNameConnector
qualityProfileId={qualityProfileId} qualityProfileIds={qualityProfileIds}
/> />
} }
</span> </span>
@@ -798,7 +798,7 @@ MovieDetails.propTypes = {
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired, sizeOnDisk: PropTypes.number.isRequired,
qualityProfileId: PropTypes.number.isRequired, qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
studio: PropTypes.string, studio: PropTypes.string,
@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import styles from './MovieReleaseDates.css';
function MovieReleaseDates(props) {
const {
showRelativeDates,
shortDateFormat,
timeFormat,
inCinemas,
physicalRelease,
digitalRelease
} = props;
return (
<div>
{
!!inCinemas &&
<div >
<div className={styles.dateIcon}>
<Icon
name={icons.IN_CINEMAS}
/>
</div>
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
</div>
}
{
!!digitalRelease &&
<div >
<div className={styles.dateIcon}>
<Icon
name={icons.MOVIE_FILE}
/>
</div>
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
</div>
}
{
!!physicalRelease &&
<div >
<div className={styles.dateIcon}>
<Icon
name={icons.DISC}
/>
</div>
{getRelativeDate(physicalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
</div>
}
</div>
);
}
MovieReleaseDates.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
inCinemas: PropTypes.string,
physicalRelease: PropTypes.string,
digitalRelease: PropTypes.string
};
export default MovieReleaseDates;
@@ -1,64 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
import styles from './MovieReleaseDates.css';
interface MovieReleaseDatesProps {
inCinemas: string;
physicalRelease: string;
digitalRelease: string;
}
function MovieReleaseDates(props: MovieReleaseDatesProps) {
const { inCinemas, physicalRelease, digitalRelease } = props;
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
return (
<div>
{inCinemas ? (
<div title={translate('InCinemas')}>
<div className={styles.dateIcon}>
<Icon name={icons.IN_CINEMAS} />
</div>
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: false,
})}
</div>
) : null}
{digitalRelease ? (
<div title={translate('DigitalRelease')}>
<div className={styles.dateIcon}>
<Icon name={icons.MOVIE_FILE} />
</div>
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: false,
})}
</div>
) : null}
{physicalRelease ? (
<div title={translate('PhysicalRelease')}>
<div className={styles.dateIcon}>
<Icon name={icons.DISC} />
</div>
{getRelativeDate(
physicalRelease,
shortDateFormat,
showRelativeDates,
{ timeFormat, timeForToday: false }
)}
</div>
) : null}
</div>
);
}
export default MovieReleaseDates;
@@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import MovieReleaseDates from './MovieReleaseDates';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
export default connect(createMapStateToProps, null)(MovieReleaseDates);
@@ -73,7 +73,7 @@ class EditMovieModalContent extends Component {
const { const {
monitored, monitored,
qualityProfileId, qualityProfileIds,
minimumAvailability, minimumAvailability,
// Id, // Id,
path, path,
@@ -114,12 +114,12 @@ class EditMovieModalContent extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel> <FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId" name="qualityProfileIds"
{...qualityProfileId} {...qualityProfileIds}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
@@ -37,7 +37,7 @@ function createMapStateToProps() {
const movieSettings = { const movieSettings = {
monitored: movie.monitored, monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId, qualityProfileIds: movie.qualityProfileIds,
minimumAvailability: movie.minimumAvailability, minimumAvailability: movie.minimumAvailability,
path: movie.path, path: movie.path,
tags: movie.tags tags: movie.tags
@@ -0,0 +1,333 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import translate from 'Utilities/String/translate';
import DeleteMovieModal from './Delete/DeleteMovieModal';
import MovieEditorFooterLabel from './MovieEditorFooterLabel';
import QualityProfilesModal from './QualityProfiles/QualityProfilesModal';
import TagsModal from './Tags/TagsModal';
import styles from './MovieEditorFooter.css';
const NO_CHANGE = 'noChange';
class MovieEditorFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
minimumAvailability: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
savingQualityProfiles: false,
isDeleteMovieModalOpen: false,
isTagsModalOpen: false,
isQualityProfilesModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
minimumAvailability: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
savingQualityProfiles: false
});
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
if (value === NO_CHANGE) {
return;
}
switch (name) {
case 'rootFolderPath':
this.setState({
isConfirmMoveModalOpen: true,
destinationRootFolder: value
});
break;
case 'monitored':
this.props.onSaveSelected({ [name]: value === 'monitored' });
break;
default:
this.props.onSaveSelected({ [name]: value });
}
};
onApplyTagsPress = (tags, applyTags) => {
this.setState({
savingTags: true,
isTagsModalOpen: false
});
this.props.onSaveSelected({
tags,
applyTags
});
};
onApplyQualityProfilesPress = (qualityProfileIds) => {
this.setState({
savingQualityProfiles: true,
isQualityProfilesModalOpen: false
});
this.props.onSaveSelected({
qualityProfileIds
});
};
onDeleteSelectedPress = () => {
this.setState({ isDeleteMovieModalOpen: true });
};
onDeleteMovieModalClose = () => {
this.setState({ isDeleteMovieModalOpen: false });
};
onTagsPress = () => {
this.setState({ isTagsModalOpen: true });
};
onTagsModalClose = () => {
this.setState({ isTagsModalOpen: false });
};
onQualityProfilesPress = () => {
this.setState({ isQualityProfilesModalOpen: true });
};
onQualityProfilesModalClose = () => {
this.setState({ isQualityProfilesModalOpen: false });
};
onSaveRootFolderPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
};
onMoveMoviePress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({
rootFolderPath: this.state.destinationRootFolder,
moveFiles: true
});
};
//
// Render
render() {
const {
movieIds,
selectedCount,
isSaving,
isDeleting,
isOrganizingMovie,
onOrganizeMoviePress
} = this.props;
const {
monitored,
qualityProfileIds,
minimumAvailability,
rootFolderPath,
savingTags,
savingQualityProfiles,
isTagsModalOpen,
isQualityProfilesModalOpen,
isDeleteMovieModalOpen,
isConfirmMoveModalOpen,
destinationRootFolder
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('MonitorMovie')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('QualityProfiles')}
isSaving={isSaving && qualityProfileIds !== NO_CHANGE}
/>
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags && savingQualityProfiles}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={this.onQualityProfilesPress}
>
{translate('SetQualityProfiles')}
</SpinnerButton>
</div>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('MinimumAvailability')}
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
/>
<AvailabilitySelectInput
name="minimumAvailability"
value={minimumAvailability}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<MovieEditorFooterLabel
label={translate('MoviesSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
isSpinning={isOrganizingMovie}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={onOrganizeMoviePress}
>
{translate('RenameFiles')}
</SpinnerButton>
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags && savingQualityProfiles}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={this.onTagsPress}
>
{translate('SetTags')}
</SpinnerButton>
</div>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
</div>
</div>
</div>
<TagsModal
isOpen={isTagsModalOpen}
movieIds={movieIds}
onApplyTagsPress={this.onApplyTagsPress}
onModalClose={this.onTagsModalClose}
/>
<QualityProfilesModal
isOpen={isQualityProfilesModalOpen}
movieIds={movieIds}
onApplyQualityProfilesPress={this.onApplyQualityProfilesPress}
onModalClose={this.onQualityProfilesModalClose}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
movieIds={movieIds}
onModalClose={this.onDeleteMovieModalClose}
/>
<MoveMovieModal
destinationRootFolder={destinationRootFolder}
isOpen={isConfirmMoveModalOpen}
onSavePress={this.onSaveRootFolderPress}
onMoveMoviePress={this.onMoveMoviePress}
/>
</PageContentFooter>
);
}
}
MovieEditorFooter.propTypes = {
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingMovie: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeMoviePress: PropTypes.func.isRequired
};
export default MovieEditorFooter;
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import QualityProfilesModalContent from './QualityProfilesModalContent';
function QualityProfilesModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<QualityProfilesModalContent
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
QualityProfilesModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default QualityProfilesModal;
@@ -0,0 +1,12 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}
@@ -0,0 +1,98 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class QualityProfilesModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
qualityProfileIds: []
};
}
//
// Lifecycle
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
};
onApplyQualityProfilesPress = () => {
const {
qualityProfileIds
} = this.state;
this.props.onApplyQualityProfilesPress(qualityProfileIds);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
qualityProfileIds
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('QualityProfiles')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
value={qualityProfileIds}
onChange={this.onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={this.onApplyQualityProfilesPress}
>
{translate('Apply')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
QualityProfilesModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onApplyQualityProfilesPress: PropTypes.func.isRequired
};
export default QualityProfilesModalContent;
@@ -100,15 +100,6 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
{translate('DigitalRelease')} {translate('DigitalRelease')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem
name="releaseDate"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('ReleaseDates')}
</SortMenuItem>
<SortMenuItem <SortMenuItem
name="tmdbRating" name="tmdbRating"
sortKey={sortKey} sortKey={sortKey}
@@ -6,6 +6,7 @@ import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
import MoviesAppState from 'App/State/MoviesAppState'; import MoviesAppState from 'App/State/MoviesAppState';
import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import { Statistics } from 'Movie/Movie';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector'; import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
@@ -17,13 +18,12 @@ function createUnoptimizedSelector() {
createClientSideCollectionSelector('movies', 'movieIndex'), createClientSideCollectionSelector('movies', 'movieIndex'),
(movies: MoviesAppState) => { (movies: MoviesAppState) => {
return movies.items.map((m) => { return movies.items.map((m) => {
const { monitored, status, hasFile, sizeOnDisk } = m; const { monitored, status, statistics = {} as Statistics } = m;
return { return {
monitored, monitored,
status, status,
hasFile, statistics,
sizeOnDisk,
}; };
}); });
} }
@@ -45,15 +45,17 @@ export default function MovieIndexFooter() {
let totalFileSize = 0; let totalFileSize = 0;
movies.forEach((s) => { movies.forEach((s) => {
if (s.hasFile) { const { statistics = { movieFileCount: 0, sizeOnDisk: 0 } } = s;
movieFiles += 1;
} const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
movieFiles += movieFileCount;
if (s.monitored) { if (s.monitored) {
monitored++; monitored++;
} }
totalFileSize += s.sizeOnDisk; totalFileSize += sizeOnDisk;
}); });
return ( return (
@@ -0,0 +1,132 @@
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 { executeCommand } from 'Store/Actions/commandActions';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
function selectShowSearchAction() {
return createSelector(
(state) => state.movieIndex,
(movieIndex) => {
const view = movieIndex.view;
switch (view) {
case 'posters':
return movieIndex.posterOptions.showSearchAction;
case 'overview':
return movieIndex.overviewOptions.showSearchAction;
default:
return movieIndex.tableOptions.showSearchAction;
}
}
);
}
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
selectShowSearchAction(),
createExecutingCommandsSelector(),
(state) => state.queue.details.items,
(
movie,
showSearchAction,
executingCommands,
queueItems
) => {
// If a movie is deleted this selector may fire before the parent
// selecors, which will result in an undefined movie, if that happens
// we want to return early here and again in the render function to avoid
// trying to show a movie that has no information available.
if (!movie) {
return {};
}
const isRefreshingMovie = executingCommands.some((command) => {
return (
command.name === commandNames.REFRESH_MOVIE &&
command.body.movieIds.includes(movie.id)
);
});
const isSearchingMovie = executingCommands.some((command) => {
return (
command.name === commandNames.MOVIE_SEARCH &&
command.body.movieIds.includes(movie.id)
);
});
const firstQueueItem = queueItems.find((q) => q.movieId === movie.id);
return {
...movie,
showSearchAction,
isRefreshingMovie,
isSearchingMovie,
queueStatus: firstQueueItem ? firstQueueItem.status : null,
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
};
}
);
}
const mapDispatchToProps = {
dispatchExecuteCommand: executeCommand
};
class MovieIndexItemConnector extends Component {
//
// Listeners
onRefreshMoviePress = () => {
this.props.dispatchExecuteCommand({
name: commandNames.REFRESH_MOVIE,
movieIds: [this.props.id]
});
};
onSearchPress = () => {
this.props.dispatchExecuteCommand({
name: commandNames.MOVIE_SEARCH,
movieIds: [this.props.id]
});
};
//
// Render
render() {
const {
id,
component: ItemComponent,
...otherProps
} = this.props;
if (!id) {
return null;
}
return (
<ItemComponent
{...otherProps}
id={id}
onRefreshMoviePress={this.onRefreshMoviePress}
onSearchPress={this.onSearchPress}
/>
);
}
}
MovieIndexItemConnector.propTypes = {
id: PropTypes.number,
component: PropTypes.elementType.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
@@ -13,6 +13,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect'; import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
@@ -67,16 +68,17 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
path, path,
overview, overview,
images, images,
hasFile,
isAvailable, isAvailable,
statistics = {} as Statistics,
tmdbId, tmdbId,
imdbId, imdbId,
studio, studio,
sizeOnDisk,
added, added,
youTubeTrailerId, youTubeTrailerId,
} = movie; } = movie;
const { movieFileCount } = statistics;
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false); const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
@@ -151,9 +153,8 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
<MovieIndexProgressBar <MovieIndexProgressBar
movieId={movieId} movieId={movieId}
movieFile={movie.movieFile} movieFileCount={movieFileCount}
monitored={monitored} monitored={monitored}
hasFile={hasFile}
isAvailable={isAvailable} isAvailable={isAvailable}
status={status} status={status}
width={posterWidth} width={posterWidth}
@@ -223,7 +224,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
monitored={monitored} monitored={monitored}
qualityProfile={qualityProfile} qualityProfile={qualityProfile}
studio={studio} studio={studio}
sizeOnDisk={sizeOnDisk} sizeOnDisk={statistics.sizeOnDisk}
added={added} added={added}
path={path} path={path}
sortKey={sortKey} sortKey={sortKey}
@@ -0,0 +1,192 @@
import PropTypes from 'prop-types';
import React from 'react';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import MovieIndexOverviewInfoRow from './MovieIndexOverviewInfoRow';
import styles from './MovieIndexOverviewInfo.css';
const infoRowHeight = parseInt(dimensions.movieIndexOverviewInfoRowHeight);
const rows = [
{
name: 'monitored',
showProp: 'showMonitored',
valueProp: 'monitored'
},
{
name: 'studio',
showProp: 'showStudio',
valueProp: 'studio'
},
{
name: 'qualityProfileId',
showProp: 'showQualityProfile',
valueProp: 'qualityProfileId'
},
{
name: 'added',
showProp: 'showAdded',
valueProp: 'added'
},
{
name: 'path',
showProp: 'showPath',
valueProp: 'path'
},
{
name: 'sizeOnDisk',
showProp: 'showSizeOnDisk',
valueProp: 'sizeOnDisk'
}
];
function isVisible(row, props) {
const {
name,
showProp,
valueProp
} = row;
if (props[valueProp] == null) {
return false;
}
return props[showProp] || props.sortKey === name;
}
function getInfoRowProps(row, props) {
const { name } = row;
if (name === 'monitored') {
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
return {
title: monitoredText,
iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED,
label: monitoredText
};
}
if (name === 'studio') {
return {
title: 'Studio',
iconName: icons.STUDIO,
label: props.studio
};
}
// if (name === 'qualityProfileId') {
// return {
// title: 'Quality Profile',
// iconName: icons.PROFILE,
// label: props.qualityProfile.name
// };
// }
if (name === 'added') {
const {
added,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat
} = props;
return {
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
iconName: icons.ADD,
label: getRelativeDate(
added,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
};
}
if (name === 'path') {
return {
title: 'Path',
iconName: icons.FOLDER,
label: props.path
};
}
if (name === 'sizeOnDisk') {
return {
title: 'Size on Disk',
iconName: icons.DRIVE,
label: formatBytes(props.sizeOnDisk)
};
}
}
function MovieIndexOverviewInfo(props) {
const {
height
// showRelativeDates,
// shortDateFormat,
// longDateFormat,
// timeFormat
} = props;
let shownRows = 1;
const maxRows = Math.floor(height / (infoRowHeight + 4));
return (
<div className={styles.infos}>
{
rows.map((row) => {
if (!isVisible(row, props)) {
return null;
}
if (shownRows >= maxRows) {
return null;
}
shownRows++;
const infoRowProps = getInfoRowProps(row, props);
return (
<MovieIndexOverviewInfoRow
key={row.name}
{...infoRowProps}
/>
);
})
}
</div>
);
}
MovieIndexOverviewInfo.propTypes = {
height: PropTypes.number.isRequired,
showStudio: PropTypes.bool.isRequired,
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
showAdded: PropTypes.bool.isRequired,
showPath: PropTypes.bool.isRequired,
showSizeOnDisk: PropTypes.bool.isRequired,
monitored: PropTypes.bool.isRequired,
studio: PropTypes.string,
qualityProfile: PropTypes.object.isRequired,
added: PropTypes.string,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number,
sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
export default MovieIndexOverviewInfo;
@@ -16,6 +16,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect'; import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -64,7 +65,6 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
tmdbId, tmdbId,
imdbId, imdbId,
youTubeTrailerId, youTubeTrailerId,
hasFile,
isAvailable, isAvailable,
studio, studio,
added, added,
@@ -73,14 +73,15 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
physicalRelease, physicalRelease,
digitalRelease, digitalRelease,
path, path,
movieFile,
ratings, ratings,
sizeOnDisk, statistics = {} as Statistics,
certification, certification,
originalTitle, originalTitle,
originalLanguage, originalLanguage,
} = movie; } = movie;
const { movieFileCount, sizeOnDisk } = statistics;
const dispatch = useDispatch(); const dispatch = useDispatch();
const [hasPosterError, setHasPosterError] = useState(false); const [hasPosterError, setHasPosterError] = useState(false);
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
@@ -213,9 +214,8 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
<MovieIndexProgressBar <MovieIndexProgressBar
movieId={movieId} movieId={movieId}
movieFile={movieFile} movieFileCount={movieFileCount}
monitored={monitored} monitored={monitored}
hasFile={hasFile}
isAvailable={isAvailable} isAvailable={isAvailable}
status={status} status={status}
width={posterWidth} width={posterWidth}
@@ -5,17 +5,15 @@ import { sizes } from 'Helpers/Props';
import createMovieQueueItemsDetailsSelector, { import createMovieQueueItemsDetailsSelector, {
MovieQueueDetails, MovieQueueDetails,
} from 'Movie/Index/createMovieQueueDetailsSelector'; } from 'Movie/Index/createMovieQueueDetailsSelector';
import { MovieFile } from 'MovieFile/MovieFile';
import getStatusStyle from 'Utilities/Movie/getStatusStyle'; import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './MovieIndexProgressBar.css'; import styles from './MovieIndexProgressBar.css';
interface MovieIndexProgressBarProps { interface MovieIndexProgressBarProps {
movieId: number; movieId: number;
movieFile: MovieFile;
monitored: boolean; monitored: boolean;
movieFileCount: number;
status: string; status: string;
hasFile: boolean;
isAvailable: boolean; isAvailable: boolean;
width: number; width: number;
detailedProgressBar: boolean; detailedProgressBar: boolean;
@@ -26,10 +24,9 @@ interface MovieIndexProgressBarProps {
function MovieIndexProgressBar(props: MovieIndexProgressBarProps) { function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
const { const {
movieId, movieId,
movieFile,
monitored, monitored,
movieFileCount,
status, status,
hasFile,
isAvailable, isAvailable,
width, width,
detailedProgressBar, detailedProgressBar,
@@ -42,6 +39,7 @@ function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
); );
const progress = 100; const progress = 100;
const hasFile = movieFileCount > 0;
const queueStatusText = const queueStatusText =
queueDetails.count > 0 ? translate('Downloading') : null; queueDetails.count > 0 ? translate('Downloading') : null;
let movieStatus = status === 'released' && hasFile ? 'downloaded' : status; let movieStatus = status === 'released' && hasFile ? 'downloaded' : status;
@@ -50,10 +48,10 @@ function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
movieStatus = translate('Missing'); movieStatus = translate('Missing');
if (hasFile) { if (hasFile) {
movieStatus = movieFile?.quality?.quality.name ?? translate('Downloaded'); movieStatus = translate('Downloaded');
} }
} else if (hasFile) { } else if (hasFile) {
movieStatus = movieFile?.quality?.quality.name ?? translate('Downloaded'); movieStatus = translate('Downloaded');
} else if (isAvailable && !hasFile) { } else if (isAvailable && !hasFile) {
movieStatus = translate('Missing'); movieStatus = translate('Missing');
} else { } else {
@@ -32,7 +32,7 @@
} }
.originalLanguage, .originalLanguage,
.qualityProfileId { .qualityProfileIds {
composes: cell; composes: cell;
flex: 1 0 125px; flex: 1 0 125px;
+1 -1
View File
@@ -19,7 +19,7 @@ interface CssExports {
'path': string; 'path': string;
'physicalRelease': string; 'physicalRelease': string;
'popularity': string; 'popularity': string;
'qualityProfileId': string; 'qualityProfileIds': string;
'rottenTomatoesRating': string; 'rottenTomatoesRating': string;
'runtime': string; 'runtime': string;
'sizeOnDisk': string; 'sizeOnDisk': string;
@@ -6,6 +6,7 @@ import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating'; import ImdbRating from 'Components/ImdbRating';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import QualityProfileList from 'Components/QualityProfileList';
import RottenTomatoRating from 'Components/RottenTomatoRating'; import RottenTomatoRating from 'Components/RottenTomatoRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
@@ -19,6 +20,7 @@ import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector'; import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
import { Statistics } from 'Movie/Movie';
import MoviePopularityIndex from 'Movie/MoviePopularityIndex'; import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
@@ -43,8 +45,9 @@ interface MovieIndexRowProps {
function MovieIndexRow(props: MovieIndexRowProps) { function MovieIndexRow(props: MovieIndexRowProps) {
const { movieId, columns, isSelectMode } = props; const { movieId, columns, isSelectMode } = props;
const { movie, qualityProfile, isRefreshingMovie, isSearchingMovie } = const { movie, isRefreshingMovie, isSearchingMovie } = useSelector(
useSelector(createMovieIndexItemSelector(props.movieId)); createMovieIndexItemSelector(props.movieId)
);
const { showSearchAction } = useSelector(selectTableOptions); const { showSearchAction } = useSelector(selectTableOptions);
@@ -66,8 +69,8 @@ function MovieIndexRow(props: MovieIndexRowProps) {
physicalRelease, physicalRelease,
runtime, runtime,
minimumAvailability, minimumAvailability,
qualityProfileIds,
path, path,
sizeOnDisk,
genres = [], genres = [],
ratings, ratings,
popularity, popularity,
@@ -76,12 +79,13 @@ function MovieIndexRow(props: MovieIndexRowProps) {
tmdbId, tmdbId,
imdbId, imdbId,
isAvailable, isAvailable,
hasFile, statistics = {} as Statistics,
movieFile,
youTubeTrailerId, youTubeTrailerId,
isSaving = false, isSaving = false,
} = movie; } = movie;
const { movieFileCount, sizeOnDisk } = statistics;
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false); const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
@@ -207,10 +211,10 @@ function MovieIndexRow(props: MovieIndexRowProps) {
); );
} }
if (name === 'qualityProfileId') { if (name === 'qualityProfileIds') {
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
{qualityProfile?.name ?? ''} <QualityProfileList qualityProfileIds={qualityProfileIds} />
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -326,9 +330,8 @@ function MovieIndexRow(props: MovieIndexRowProps) {
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
<MovieIndexProgressBar <MovieIndexProgressBar
movieId={movieId} movieId={movieId}
movieFile={movieFile} movieFileCount={movieFileCount}
monitored={monitored} monitored={monitored}
hasFile={hasFile}
isAvailable={isAvailable} isAvailable={isAvailable}
status={status} status={status}
width={125} width={125}
@@ -25,7 +25,7 @@
} }
.originalLanguage, .originalLanguage,
.qualityProfileId { .qualityProfileIds {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 1 0 125px; flex: 1 0 125px;
@@ -16,7 +16,7 @@ interface CssExports {
'path': string; 'path': string;
'physicalRelease': string; 'physicalRelease': string;
'popularity': string; 'popularity': string;
'qualityProfileId': string; 'qualityProfileIds': string;
'rottenTomatoesRating': string; 'rottenTomatoesRating': string;
'runtime': string; 'runtime': string;
'sizeOnDisk': string; 'sizeOnDisk': string;
+9 -5
View File
@@ -1,6 +1,5 @@
import ModelBase from 'App/ModelBase'; import ModelBase from 'App/ModelBase';
import Language from 'Language/Language'; import Language from 'Language/Language';
import { MovieFile } from 'MovieFile/MovieFile';
export interface Image { export interface Image {
coverType: string; coverType: string;
@@ -19,6 +18,12 @@ export interface Ratings {
rottenTomatoes: object; rottenTomatoes: object;
} }
export interface Statistics {
movieFileCount: number;
releaseGroups: string[];
sizeOnDisk: number;
}
interface Movie extends ModelBase { interface Movie extends ModelBase {
tmdbId: number; tmdbId: number;
imdbId: string; imdbId: string;
@@ -31,7 +36,8 @@ interface Movie extends ModelBase {
titleSlug: string; titleSlug: string;
collection: Collection; collection: Collection;
studio: string; studio: string;
qualityProfileId: number; qualityProfileIds: number[];
qualityProfile: object;
added: string; added: string;
year: number; year: number;
inCinemas: string; inCinemas: string;
@@ -42,15 +48,13 @@ interface Movie extends ModelBase {
runtime: number; runtime: number;
minimumAvailability: string; minimumAvailability: string;
path: string; path: string;
sizeOnDisk: number;
genres: string[]; genres: string[];
ratings: Ratings; ratings: Ratings;
popularity: number; popularity: number;
certification: string; certification: string;
tags: number[]; tags: number[];
images: Image[]; images: Image[];
movieFile: MovieFile; statistics: Statistics;
hasFile: boolean;
isAvailable: boolean; isAvailable: boolean;
isSaving?: boolean; isSaving?: boolean;
} }
+96
View File
@@ -0,0 +1,96 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import getQueueStatusText from 'Utilities/Movie/getQueueStatusText';
import translate from 'Utilities/String/translate';
import styles from './MovieFileStatus.css';
function MovieFileStatus(props) {
const {
isAvailable,
monitored,
queueStatus,
queueState,
statistics,
colorImpairedMode
} = props;
const {
movieFileCount
} = statistics;
const hasMovieFile = movieFileCount > 0;
const hasReleased = isAvailable;
if (queueStatus) {
const queueStatusText = getQueueStatusText(queueStatus, queueState);
return (
<div className={styles.center}>
<span className={styles.queue} />
{queueStatusText}
</div>
);
}
if (hasMovieFile) {
return (
<div className={styles.center}>
<span className={styles.ended} />
Downloaded
</div>
);
}
if (!monitored) {
return (
<div className={classNames(
styles.center,
styles.missingUnmonitoredBackground,
colorImpairedMode && 'colorImpaired'
)}
>
<span className={styles.missingUnmonitored} />
{translate('NotMonitored')}
</div>
);
}
if (hasReleased) {
return (
<div className={classNames(
styles.center,
styles.missingMonitoredBackground,
colorImpairedMode && 'colorImpaired'
)}
>
<span className={styles.missingMonitored} />
{translate('Missing')}
</div>
);
}
return (
<div className={styles.center}>
<span className={styles.continuing} />
{translate('NotAvailable')}
</div>
);
}
MovieFileStatus.propTypes = {
isAvailable: PropTypes.bool,
monitored: PropTypes.bool.isRequired,
statistics: PropTypes.object,
queueStatus: PropTypes.string,
queueState: PropTypes.string,
colorImpairedMode: PropTypes.bool.isRequired
};
MovieFileStatus.defaultProps = {
statistics: {
movieFileCount: 0
}
};
export default MovieFileStatus;
@@ -0,0 +1,49 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import MovieFileStatus from './MovieFileStatus';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
return {
inCinemas: movie.inCinemas,
isAvailable: movie.isAvailable,
monitored: movie.monitored,
grabbed: movie.grabbed,
statistics: movie.statistics,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
}
const mapDispatchToProps = {
};
class MovieFileStatusConnector extends Component {
//
// Render
render() {
return (
<MovieFileStatus
{...this.props}
/>
);
}
}
MovieFileStatusConnector.propTypes = {
movieId: PropTypes.number.isRequired,
queueStatus: PropTypes.string,
queueState: PropTypes.string
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieFileStatusConnector);
@@ -80,12 +80,8 @@ function DownloadClientOptions(props) {
legend={translate('FailedDownloadHandling')} legend={translate('FailedDownloadHandling')}
> >
<Form> <Form>
<FormGroup <FormGroup size={sizes.MEDIUM}>
advancedSettings={advancedSettings} <FormLabel>{translate('Redownload')}</FormLabel>
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
@@ -95,28 +91,7 @@ function DownloadClientOptions(props) {
{...settings.autoRedownloadFailed} {...settings.autoRedownloadFailed}
/> />
</FormGroup> </FormGroup>
{
settings.autoRedownloadFailed.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="autoRedownloadFailedFromInteractiveSearch"
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
onChange={onInputChange}
{...settings.autoRedownloadFailedFromInteractiveSearch}
/>
</FormGroup> :
null
}
</Form> </Form>
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
{translate('RemoveDownloadsAlert')} {translate('RemoveDownloadsAlert')}
</Alert> </Alert>
@@ -107,6 +107,7 @@ class GeneralSettings extends Component {
packageUpdateMechanism, packageUpdateMechanism,
onInputChange, onInputChange,
onConfirmResetApiKey, onConfirmResetApiKey,
plexServersPopulated,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -145,6 +146,7 @@ class GeneralSettings extends Component {
<SecuritySettings <SecuritySettings
settings={settings} settings={settings}
plexServersPopulated={plexServersPopulated}
isResettingApiKey={isResettingApiKey} isResettingApiKey={isResettingApiKey}
onInputChange={onInputChange} onInputChange={onInputChange}
onConfirmResetApiKey={onConfirmResetApiKey} onConfirmResetApiKey={onConfirmResetApiKey}
@@ -202,6 +204,7 @@ class GeneralSettings extends Component {
GeneralSettings.propTypes = { GeneralSettings.propTypes = {
advancedSettings: PropTypes.bool.isRequired, advancedSettings: PropTypes.bool.isRequired,
plexServersPopulated: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
@@ -17,12 +17,14 @@ const SECTION = 'general';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
(state) => state.settings.plex,
createSettingsSectionSelector(SECTION), createSettingsSectionSelector(SECTION),
createCommandExecutingSelector(commandNames.RESET_API_KEY), createCommandExecutingSelector(commandNames.RESET_API_KEY),
createSystemStatusSelector(), createSystemStatusSelector(),
(advancedSettings, sectionSettings, isResettingApiKey, systemStatus) => { (advancedSettings, plexSettings, sectionSettings, isResettingApiKey, systemStatus) => {
return { return {
advancedSettings, advancedSettings,
plexServersPopulated: plexSettings.isPopulated,
isResettingApiKey, isResettingApiKey,
isWindows: systemStatus.isWindows, isWindows: systemStatus.isWindows,
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service', isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
+135 -22
View File
@@ -5,6 +5,7 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton'; import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import OAuthInputConnector from 'Components/Form/OAuthInputConnector';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ClipboardButton from 'Components/Link/ClipboardButton'; import ClipboardButton from 'Components/Link/ClipboardButton';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -37,6 +38,18 @@ export const authenticationMethodOptions = [
get value() { get value() {
return translate('AuthForm'); return translate('AuthForm');
} }
},
{
key: 'plex',
get value() {
return translate('AuthPlex');
}
},
{
key: 'oidc',
get value() {
return translate('AuthOidc');
}
} }
]; ];
@@ -76,6 +89,22 @@ const certificateValidationOptions = [
} }
]; ];
const oauthData = {
implementation: { value: 'PlexImport' },
configContract: { value: 'PlexListSettings' },
fields: [
{
type: 'textbox',
name: 'accessToken'
},
{
type: 'oAuth',
name: 'signIn',
value: 'startAuth'
}
]
};
class SecuritySettings extends Component { class SecuritySettings extends Component {
// //
@@ -115,6 +144,7 @@ class SecuritySettings extends Component {
render() { render() {
const { const {
settings, settings,
plexServersPopulated,
isResettingApiKey, isResettingApiKey,
onInputChange onInputChange
} = this.props; } = this.props;
@@ -124,11 +154,19 @@ class SecuritySettings extends Component {
authenticationRequired, authenticationRequired,
username, username,
password, password,
plexAuthServer,
plexRequireOwner,
oidcClientId,
oidcClientSecret,
oidcAuthority,
apiKey, apiKey,
certificateValidation certificateValidation
} = settings; } = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const showUserPass = authenticationMethod && ['basic', 'forms'].includes(authenticationMethod.value);
const plexEnabled = authenticationMethod && authenticationMethod.value === 'plex';
const oidcEnabled = authenticationMethod && authenticationMethod.value === 'oidc';
return ( return (
<FieldSet legend={translate('Security')}> <FieldSet legend={translate('Security')}>
@@ -164,33 +202,107 @@ class SecuritySettings extends Component {
} }
{ {
authenticationEnabled ? showUserPass &&
<FormGroup> <>
<FormLabel>{translate('Username')}</FormLabel> <FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="username" name="username"
onChange={onInputChange} onChange={onInputChange}
{...username} {...username}
/> />
</FormGroup> : </FormGroup>
null
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup>
</>
} }
{ {
authenticationEnabled ? plexEnabled &&
<FormGroup> <>
<FormLabel>{translate('Password')}</FormLabel> <FormGroup>
<FormLabel>{translate('PlexServer')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.PASSWORD} type={inputTypes.PLEX_MACHINE_SELECT}
name="password" name="plexAuthServer"
onChange={onInputChange} buttons={[
{...password} <FormInputButton
/> key="auth"
</FormGroup> : ButtonComponent={OAuthInputConnector}
null label={plexServersPopulated ? <Icon name={icons.REFRESH} /> : 'Fetch'}
name="plexAuth"
provider="importList"
providerData={oauthData}
section="settings.importLists"
onChange={onInputChange}
/>
]}
onChange={onInputChange}
{...plexAuthServer}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RestrictAccessToServerOwner')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="plexRequireOwner"
onChange={onInputChange}
{...plexRequireOwner}
/>
</FormGroup>
</>
}
{
oidcEnabled &&
<>
<FormGroup>
<FormLabel>{translate('Authority')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcAuthority"
onChange={onInputChange}
{...oidcAuthority}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ClientId')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcClientId"
onChange={onInputChange}
{...oidcClientId}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ClientSecret')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="oidcClientSecret"
onChange={onInputChange}
{...oidcClientSecret}
/>
</FormGroup>
</>
} }
<FormGroup> <FormGroup>
@@ -254,6 +366,7 @@ class SecuritySettings extends Component {
SecuritySettings.propTypes = { SecuritySettings.propTypes = {
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
plexServersPopulated: PropTypes.bool.isRequired,
isResettingApiKey: PropTypes.bool.isRequired, isResettingApiKey: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
onConfirmResetApiKey: PropTypes.func.isRequired onConfirmResetApiKey: PropTypes.func.isRequired
@@ -46,7 +46,7 @@ function EditImportListModalContent(props) {
minRefreshInterval, minRefreshInterval,
monitor, monitor,
minimumAvailability, minimumAvailability,
qualityProfileId, qualityProfileIds,
rootFolderPath, rootFolderPath,
searchOnAdd, searchOnAdd,
tags, tags,
@@ -169,8 +169,8 @@ function EditImportListModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId" name="qualityProfileIds"
{...qualityProfileId} {...qualityProfileIds}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>

Some files were not shown because too many files have changed in this diff Show More