mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-22 17:04:39 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17c6d11a47 | ||
|
|
a994502e59 | ||
|
|
4154414adf | ||
|
|
c5b12d074e | ||
|
|
578aa14770 | ||
|
|
da1bc0aa88 | ||
|
|
a56d827744 | ||
|
|
a53234a107 | ||
|
|
a015fa3255 | ||
|
|
b2cb95829c | ||
|
|
64fd60a7d5 | ||
|
|
c3368d9e6c | ||
|
|
583c5d501c | ||
|
|
af6b16ec57 | ||
|
|
7ac5a5843b | ||
|
|
2465815890 |
@@ -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: '4.3.1'
|
majorVersion: '5.0.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
@@ -27,6 +27,7 @@ trigger:
|
|||||||
include:
|
include:
|
||||||
- develop
|
- develop
|
||||||
- master
|
- master
|
||||||
|
- zeus
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
branches:
|
branches:
|
||||||
@@ -1092,7 +1093,7 @@ stages:
|
|||||||
projectVersion: '$(radarrVersion)'
|
projectVersion: '$(radarrVersion)'
|
||||||
extraProperties: |
|
extraProperties: |
|
||||||
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
||||||
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
|
sonar.coverage.exclusions=**/Radarr.Api.V*/**/*
|
||||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||||
- bash: |
|
- bash: |
|
||||||
|
|||||||
2
docs.sh
2
docs.sh
@@ -29,7 +29,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
|||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
|
dotnet tool run swagger tofile --output ./src/Radarr.Api.V4/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v4 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -120,7 +124,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
isExistingMovie &&
|
isExistingMovie &&
|
||||||
<MovieIndexProgressBar
|
<MovieIndexProgressBar
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
hasFile={hasFile}
|
hasFile={movieFileCount > 0}
|
||||||
status={status}
|
status={status}
|
||||||
posterWidth={posterWidth}
|
posterWidth={posterWidth}
|
||||||
detailedProgressBar={true}
|
detailedProgressBar={true}
|
||||||
@@ -233,7 +237,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}
|
||||||
@@ -290,7 +294,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>
|
||||||
@@ -251,10 +251,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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,7 +24,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) &&
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
$fullColorGradient: rgba(244, 245, 246, 0.2);
|
||||||
|
|
||||||
.event {
|
.event {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin: 4px 2px;
|
margin: 4px 2px;
|
||||||
@@ -55,6 +57,10 @@
|
|||||||
.downloaded {
|
.downloaded {
|
||||||
border-left-color: var(--successColor) !important;
|
border-left-color: var(--successColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(39, 194, 76, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&:global(.colorImpaired) {
|
||||||
border-left-color: color(var(--successColor), saturation(+15%)) !important;
|
border-left-color: color(var(--successColor), saturation(+15%)) !important;
|
||||||
}
|
}
|
||||||
@@ -62,28 +68,72 @@
|
|||||||
|
|
||||||
.queue {
|
.queue {
|
||||||
border-left-color: var(--purple) !important;
|
border-left-color: var(--purple) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(122, 67, 182, 0.4) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.unmonitored {
|
.unmonitored {
|
||||||
border-left-color: var(--gray) !important;
|
border-left-color: var(--gray) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(173, 173, 173, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingUnmonitored {
|
.missingUnmonitored {
|
||||||
border-left-color: var(--warningColor) !important;
|
border-left-color: var(--warningColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(255, 165, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&:global(.colorImpaired) {
|
||||||
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingMonitored {
|
.missingMonitored {
|
||||||
border-left-color: var(--dangerColor) !important;
|
border-left-color: var(--dangerColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(240, 80, 80, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&:global(.colorImpaired) {
|
||||||
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.continuing {
|
.unaired {
|
||||||
border-left-color: var(--primaryColor) !important;
|
border-left-color: var(--primaryColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(93, 156, 236, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class CalendarEvent extends Component {
|
|||||||
queueItem,
|
queueItem,
|
||||||
showMovieInformation,
|
showMovieInformation,
|
||||||
showCutoffUnmetIcon,
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
colorImpairedMode,
|
colorImpairedMode,
|
||||||
date
|
date
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -62,7 +63,8 @@ class CalendarEvent extends Component {
|
|||||||
styles.event,
|
styles.event,
|
||||||
styles.link,
|
styles.link,
|
||||||
styles[statusStyle],
|
styles[statusStyle],
|
||||||
colorImpairedMode && 'colorImpaired'
|
colorImpairedMode && 'colorImpaired',
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
)}
|
)}
|
||||||
// component="div"
|
// component="div"
|
||||||
to={link}
|
to={link}
|
||||||
@@ -97,7 +99,7 @@ class CalendarEvent extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.MOVIE_FILE}
|
name={icons.MOVIE_FILE}
|
||||||
kind={kinds.WARNING}
|
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||||
title={translate('QualityCutoffHasNotBeenMet')}
|
title={translate('QualityCutoffHasNotBeenMet')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -142,11 +144,12 @@ CalendarEvent.propTypes = {
|
|||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
certification: PropTypes.string,
|
certification: PropTypes.string,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
hasFile: PropTypes.bool,
|
||||||
grabbed: PropTypes.bool,
|
grabbed: PropTypes.bool,
|
||||||
queueItem: PropTypes.object,
|
queueItem: PropTypes.object,
|
||||||
showMovieInformation: PropTypes.bool.isRequired,
|
showMovieInformation: PropTypes.bool.isRequired,
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired,
|
colorImpairedMode: PropTypes.bool.isRequired,
|
||||||
date: PropTypes.string.isRequired
|
date: PropTypes.string.isRequired
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import styles from './Legend.css';
|
|||||||
function Legend(props) {
|
function Legend(props) {
|
||||||
const {
|
const {
|
||||||
showCutoffUnmetIcon,
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
colorImpairedMode
|
colorImpairedMode
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ function Legend(props) {
|
|||||||
<LegendIconItem
|
<LegendIconItem
|
||||||
name={translate('CutoffUnmet')}
|
name={translate('CutoffUnmet')}
|
||||||
icon={icons.MOVIE_FILE}
|
icon={icons.MOVIE_FILE}
|
||||||
kind={kinds.WARNING}
|
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||||
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
|
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -31,12 +32,14 @@ function Legend(props) {
|
|||||||
<LegendItem
|
<LegendItem
|
||||||
style='ended'
|
style='ended'
|
||||||
name={translate('DownloadedAndMonitored')}
|
name={translate('DownloadedAndMonitored')}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='availNotMonitored'
|
style='availNotMonitored'
|
||||||
name={translate('DownloadedButNotMonitored')}
|
name={translate('DownloadedButNotMonitored')}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,12 +48,14 @@ function Legend(props) {
|
|||||||
<LegendItem
|
<LegendItem
|
||||||
style='missingMonitored'
|
style='missingMonitored'
|
||||||
name={translate('MissingMonitoredAndConsideredAvailable')}
|
name={translate('MissingMonitoredAndConsideredAvailable')}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='missingUnmonitored'
|
style='missingUnmonitored'
|
||||||
name={translate('MissingNotMonitored')}
|
name={translate('MissingNotMonitored')}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,12 +64,14 @@ function Legend(props) {
|
|||||||
<LegendItem
|
<LegendItem
|
||||||
style='queue'
|
style='queue'
|
||||||
name={translate('Queued')}
|
name={translate('Queued')}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='continuing'
|
style='continuing'
|
||||||
name={translate('Unreleased')}
|
name={translate('Unreleased')}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +86,9 @@ function Legend(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Legend.propTypes = {
|
Legend.propTypes = {
|
||||||
|
view: PropTypes.string.isRequired,
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
colorImpairedMode: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import Legend from './Legend';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.calendar.options,
|
(state) => state.calendar.options,
|
||||||
|
(state) => state.calendar.view,
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
(calendarOptions, uiSettings) => {
|
(calendarOptions, view, uiSettings) => {
|
||||||
return {
|
return {
|
||||||
...calendarOptions,
|
...calendarOptions,
|
||||||
|
view,
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ function LegendIconItem(props) {
|
|||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
kind,
|
kind,
|
||||||
|
darken,
|
||||||
tooltip
|
tooltip
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ function LegendIconItem(props) {
|
|||||||
<Icon
|
<Icon
|
||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
name={icon}
|
name={icon}
|
||||||
|
darken={darken}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -31,7 +33,12 @@ LegendIconItem.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
icon: PropTypes.object.isRequired,
|
icon: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
|
darken: PropTypes.bool.isRequired,
|
||||||
tooltip: PropTypes.string.isRequired
|
tooltip: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LegendIconItem.defaultProps = {
|
||||||
|
darken: false
|
||||||
|
};
|
||||||
|
|
||||||
export default LegendIconItem;
|
export default LegendIconItem;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
$fullColorGradient: rgba(244, 245, 246, 0.2);
|
||||||
|
|
||||||
.legendItemContainer {
|
.legendItemContainer {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
@@ -63,10 +65,12 @@
|
|||||||
|
|
||||||
.missingMonitoredColorImpaired {
|
.missingMonitoredColorImpaired {
|
||||||
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingUnmonitoredColorImpaired {
|
.missingUnmonitoredColorImpaired {
|
||||||
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legendItemText {
|
.legendItemText {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ function LegendItem(props) {
|
|||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
|
fullColorEvents,
|
||||||
colorImpairedMode
|
colorImpairedMode
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ function LegendItem(props) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
styles.legendItem,
|
styles.legendItem,
|
||||||
styles[style],
|
styles[style],
|
||||||
colorImpairedMode && 'colorImpaired'
|
colorImpairedMode && 'colorImpaired',
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}>
|
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}>
|
||||||
@@ -29,6 +31,7 @@ function LegendItem(props) {
|
|||||||
LegendItem.propTypes = {
|
LegendItem.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
style: PropTypes.string.isRequired,
|
style: PropTypes.string.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
colorImpairedMode: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
firstDayOfWeek,
|
firstDayOfWeek,
|
||||||
calendarWeekColumnHeader,
|
calendarWeekColumnHeader,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode,
|
||||||
|
fullColorEvents
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
firstDayOfWeek,
|
firstDayOfWeek,
|
||||||
calendarWeekColumnHeader,
|
calendarWeekColumnHeader,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode,
|
||||||
|
fullColorEvents
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
showMovieInformation,
|
showMovieInformation,
|
||||||
showCutoffUnmetIcon,
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -136,6 +139,18 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
onChange={this.onOptionInputChange}
|
onChange={this.onOptionInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('FullColorEvents')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="fullColorEvents"
|
||||||
|
value={fullColorEvents}
|
||||||
|
helpText={translate('FullColorEventsHelpText')}
|
||||||
|
onChange={this.onOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</Form>
|
</Form>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
@@ -176,7 +191,9 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
value={timeFormat}
|
value={timeFormat}
|
||||||
onChange={this.onGlobalInputChange}
|
onChange={this.onGlobalInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup><FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
|
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
@@ -187,7 +204,6 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
onChange={this.onGlobalInputChange}
|
onChange={this.onGlobalInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@@ -209,6 +225,7 @@ CalendarOptionsModalContent.propTypes = {
|
|||||||
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
dispatchSetCalendarOption: PropTypes.func.isRequired,
|
dispatchSetCalendarOption: PropTypes.func.isRequired,
|
||||||
dispatchSaveUISettings: PropTypes.func.isRequired,
|
dispatchSaveUISettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function getUrls(state) {
|
|||||||
tags
|
tags
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
|
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v4/calendar/Radarr.ics?`;
|
||||||
|
|
||||||
if (unmonitored) {
|
if (unmonitored) {
|
||||||
icalUrl += 'unmonitored=true&';
|
icalUrl += 'unmonitored=true&';
|
||||||
|
|||||||
@@ -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: []
|
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,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class EditCollectionModalContent extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
monitored,
|
monitored,
|
||||||
qualityProfileId,
|
qualityProfileIds,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
// Id,
|
// Id,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
@@ -104,12 +104,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,
|
||||||
searchOnAdd: collection.searchOnAdd
|
searchOnAdd: collection.searchOnAdd
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ class CollectionMovieLabel extends Component {
|
|||||||
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}>
|
||||||
@@ -46,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>
|
||||||
}
|
}
|
||||||
@@ -63,9 +65,9 @@ CollectionMovieLabel.propTypes = {
|
|||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -20,6 +20,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';
|
||||||
@@ -62,6 +63,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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
44
frontend/src/Components/Form/PlexMachineInput.js
Normal file
44
frontend/src/Components/Form/PlexMachineInput.js
Normal file
@@ -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;
|
||||||
115
frontend/src/Components/Form/PlexMachineInputConnector.js
Normal file
115
frontend/src/Components/Form/PlexMachineInputConnector.js
Normal file
@@ -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);
|
||||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import SelectInput from './SelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@@ -45,40 +45,14 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
class QualityProfileSelectInputConnector extends Component {
|
class QualityProfileSelectInputConnector extends Component {
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!value || !values.some((v) => v.key === value) ) {
|
|
||||||
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
|
|
||||||
|
|
||||||
if (firstValue) {
|
|
||||||
this.onChange({ name, value: firstValue.key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<EnhancedSelectInput
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onChange={this.onChange}
|
onChange={this.props.onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,7 +60,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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -12,10 +12,18 @@
|
|||||||
|
|
||||||
.info {
|
.info {
|
||||||
color: var(--infoColor);
|
color: var(--infoColor);
|
||||||
|
|
||||||
|
&:global(.darken) {
|
||||||
|
color: color(var(--infoColor) shade(30%));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pink {
|
.pink {
|
||||||
color: var(--pink);
|
color: var(--pink);
|
||||||
|
|
||||||
|
&:global(.darken) {
|
||||||
|
color: color(var(--pink) shade(30%));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Icon extends PureComponent {
|
|||||||
kind,
|
kind,
|
||||||
size,
|
size,
|
||||||
title,
|
title,
|
||||||
|
darken,
|
||||||
isSpinning,
|
isSpinning,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -26,7 +27,8 @@ class Icon extends PureComponent {
|
|||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
styles[kind]
|
styles[kind],
|
||||||
|
darken && 'darken'
|
||||||
)}
|
)}
|
||||||
icon={name}
|
icon={name}
|
||||||
spin={isSpinning}
|
spin={isSpinning}
|
||||||
@@ -59,6 +61,7 @@ Icon.propTypes = {
|
|||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
darken: PropTypes.bool.isRequired,
|
||||||
isSpinning: PropTypes.bool.isRequired,
|
isSpinning: PropTypes.bool.isRequired,
|
||||||
fixedWidth: PropTypes.bool.isRequired
|
fixedWidth: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
@@ -66,6 +69,7 @@ Icon.propTypes = {
|
|||||||
Icon.defaultProps = {
|
Icon.defaultProps = {
|
||||||
kind: kinds.DEFAULT,
|
kind: kinds.DEFAULT,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
darken: false,
|
||||||
isSpinning: false,
|
isSpinning: false,
|
||||||
fixedWidth: false
|
fixedWidth: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -56,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>
|
||||||
@@ -80,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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
|||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import PageHeader from './Header/PageHeader';
|
import PageHeader from './Header/PageHeader';
|
||||||
import PageSidebar from './Sidebar/PageSidebar';
|
import PageSidebar from './Sidebar/PageSidebar';
|
||||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
|||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
|
authenticationEnabled,
|
||||||
onSidebarToggle,
|
onSidebarToggle,
|
||||||
onSidebarVisibleChange
|
onSidebarVisibleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -109,6 +111,10 @@ class Page extends Component {
|
|||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AuthenticationRequiredModal
|
||||||
|
isOpen={!authenticationEnabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ColorImpairedContext.Provider>
|
</ColorImpairedContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -124,6 +130,7 @@ Page.propTypes = {
|
|||||||
isUpdated: PropTypes.bool.isRequired,
|
isUpdated: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
authenticationEnabled: PropTypes.bool.isRequired,
|
||||||
onResize: PropTypes.func.isRequired,
|
onResize: PropTypes.func.isRequired,
|
||||||
onSidebarToggle: PropTypes.func.isRequired,
|
onSidebarToggle: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfil
|
|||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
@@ -133,18 +134,21 @@ function createMapStateToProps() {
|
|||||||
selectErrors,
|
selectErrors,
|
||||||
selectAppProps,
|
selectAppProps,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
(
|
(
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
errors,
|
errors,
|
||||||
app,
|
app,
|
||||||
dimensions
|
dimensions,
|
||||||
|
systemStatus
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
...errors,
|
...errors,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
3
frontend/src/Components/QualityProfileList.css
Normal file
3
frontend/src/Components/QualityProfileList.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.tags {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
38
frontend/src/Components/QualityProfileList.js
Normal file
38
frontend/src/Components/QualityProfileList.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Label from './Label';
|
||||||
|
import styles from './QualityProfileList.css';
|
||||||
|
|
||||||
|
function QualityProfileList({ qualityProfileIds, 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityProfileList.propTypes = {
|
||||||
|
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
qualityProfileList: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QualityProfileList;
|
||||||
16
frontend/src/Components/QualityProfileListConnector.js
Normal file
16
frontend/src/Components/QualityProfileListConnector.js
Normal file
@@ -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);
|
||||||
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||||
|
|
||||||
|
function onModalClose() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AuthenticationRequiredModalContentConnector
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModal;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.authRequiredAlert {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
267
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
267
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputButton from 'Components/Form/FormInputButton';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import OAuthInputConnector from 'Components/Form/OAuthInputConnector';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
|
||||||
|
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() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModalContent(props) {
|
||||||
|
const {
|
||||||
|
isPopulated,
|
||||||
|
plexServersPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
settings,
|
||||||
|
onInputChange,
|
||||||
|
onSavePress,
|
||||||
|
dispatchFetchStatus
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
plexAuthServer,
|
||||||
|
plexRequireOwner,
|
||||||
|
oidcClientId,
|
||||||
|
oidcClientSecret,
|
||||||
|
oidcAuthority
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSaving && didMount.current) {
|
||||||
|
dispatchFetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
didMount.current = true;
|
||||||
|
}, [isSaving, dispatchFetchStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent
|
||||||
|
showCloseButton={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
Authentication Required
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Alert
|
||||||
|
className={styles.authRequiredAlert}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
>
|
||||||
|
{authenticationRequiredWarning}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error ?
|
||||||
|
<div>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Authentication</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationMethod"
|
||||||
|
values={authenticationMethodOptions}
|
||||||
|
helpText="Require login to access Sonarr"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationMethod}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Authentication Required</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText="Change which requests authentication is required for. Do not change unless you understand the risks."
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationRequired}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showUserPass &&
|
||||||
|
<>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="username"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...username}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="password"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...password}
|
||||||
|
/>
|
||||||
|
</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> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!authenticationEnabled}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModalContent.propTypes = {
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
plexServersPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
settings: PropTypes.object.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModalContent;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||||
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
|
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||||
|
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||||
|
|
||||||
|
const SECTION = 'general';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createSettingsSectionSelector(SECTION),
|
||||||
|
(state) => state.settings.plex,
|
||||||
|
(sectionSettings, plex) => {
|
||||||
|
return {
|
||||||
|
...sectionSettings,
|
||||||
|
plexServersPopulated: plex.isPopulated
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchClearPendingChanges: clearPendingChanges,
|
||||||
|
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||||
|
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||||
|
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||||
|
dispatchFetchStatus: fetchStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
class AuthenticationRequiredModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchGeneralSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.dispatchSaveGeneralSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearPendingChanges,
|
||||||
|
dispatchFetchGeneralSettings,
|
||||||
|
dispatchSetGeneralSettingsValue,
|
||||||
|
dispatchSaveGeneralSettings,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationRequiredModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||||
|
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||||
|
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||||
@@ -9,6 +9,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 ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||||
@@ -35,6 +36,7 @@ export const all = [
|
|||||||
OAUTH,
|
OAUTH,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
PATH,
|
PATH,
|
||||||
|
PLEX_MACHINE_SELECT,
|
||||||
QUALITY_PROFILE_SELECT,
|
QUALITY_PROFILE_SELECT,
|
||||||
DOWNLOAD_CLIENT_SELECT,
|
DOWNLOAD_CLIENT_SELECT,
|
||||||
ROOT_FOLDER_SELECT,
|
ROOT_FOLDER_SELECT,
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||||
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
||||||
import styles from './InteractiveSearchContent.css';
|
import styles from './InteractiveSearch.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -22,20 +25,6 @@ const columns = [
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'releaseWeight',
|
|
||||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
|
||||||
isSortable: true,
|
|
||||||
fixedSortDirection: sortDirections.ASCENDING,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rejections',
|
|
||||||
label: React.createElement(Icon, { name: icons.DANGER }),
|
|
||||||
isSortable: true,
|
|
||||||
fixedSortDirection: sortDirections.ASCENDING,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
label: translate('Title'),
|
label: translate('Title'),
|
||||||
@@ -99,10 +88,24 @@ const columns = [
|
|||||||
label: React.createElement(Icon, { name: icons.FLAG }),
|
label: React.createElement(Icon, { name: icons.FLAG }),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejections',
|
||||||
|
label: React.createElement(Icon, { name: icons.DANGER }),
|
||||||
|
isSortable: true,
|
||||||
|
fixedSortDirection: sortDirections.ASCENDING,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'releaseWeight',
|
||||||
|
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||||
|
isSortable: true,
|
||||||
|
fixedSortDirection: sortDirections.ASCENDING,
|
||||||
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
function InteractiveSearchContent(props) {
|
function InteractiveSearch(props) {
|
||||||
const {
|
const {
|
||||||
searchPayload,
|
searchPayload,
|
||||||
isFetching,
|
isFetching,
|
||||||
@@ -110,44 +113,63 @@ function InteractiveSearchContent(props) {
|
|||||||
error,
|
error,
|
||||||
totalReleasesCount,
|
totalReleasesCount,
|
||||||
items,
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
longDateFormat,
|
longDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
onSortPress,
|
onSortPress,
|
||||||
|
onFilterSelect,
|
||||||
onGrabPress
|
onGrabPress
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<div className={styles.filterMenuContainer}>
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
buttonComponent={PageMenuButton}
|
||||||
|
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||||
|
filterModalConnectorComponentProps={'movies'}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
isFetching &&
|
isFetching ? <LoadingIndicator /> : null
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && error ?
|
||||||
<div className={styles.blankpad}>
|
<div>
|
||||||
{translate('UnableToLoadResultsIntSearch')}
|
{translate('UnableToLoadResultsIntSearch')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && isPopulated && !totalReleasesCount &&
|
!isFetching && isPopulated && !totalReleasesCount ?
|
||||||
<div className={styles.blankpad}>
|
<div>
|
||||||
{translate('NoResultsFound')}
|
{translate('NoResultsFound')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!totalReleasesCount && isPopulated && !items.length &&
|
!!totalReleasesCount && isPopulated && !items.length ?
|
||||||
<div className={styles.blankpad}>
|
<div>
|
||||||
{translate('AllResultsHiddenFilter')}
|
{translate('AllResultsHiddenFilter')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !!items.length &&
|
isPopulated && !!items.length ?
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
@@ -170,32 +192,38 @@ function InteractiveSearchContent(props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
totalReleasesCount !== items.length && !!items.length &&
|
totalReleasesCount !== items.length && !!items.length ?
|
||||||
<div className={styles.filteredMessage}>
|
<div className={styles.filteredMessage}>
|
||||||
{translate('SomeResultsHiddenFilter')}
|
{translate('SomeResultsHiddenFilter')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveSearchContent.propTypes = {
|
InteractiveSearch.propTypes = {
|
||||||
searchPayload: PropTypes.object.isRequired,
|
searchPayload: PropTypes.object.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
totalReleasesCount: PropTypes.number.isRequired,
|
totalReleasesCount: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.string,
|
sortDirection: PropTypes.string,
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onGrabPress: PropTypes.func.isRequired
|
onGrabPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InteractiveSearchContent;
|
export default InteractiveSearch;
|
||||||
@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
|
|||||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import InteractiveSearchContent from './InteractiveSearchContent';
|
import InteractiveSearch from './InteractiveSearch';
|
||||||
|
|
||||||
function createMapStateToProps(appState) {
|
function createMapStateToProps(appState) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@@ -48,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveSearchContentConnector extends Component {
|
class InteractiveSearchConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@@ -79,18 +79,18 @@ class InteractiveSearchContentConnector extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<InteractiveSearchContent
|
<InteractiveSearch
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveSearchContentConnector.propTypes = {
|
InteractiveSearchConnector.propTypes = {
|
||||||
searchPayload: PropTypes.object.isRequired,
|
searchPayload: PropTypes.object.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||||
dispatchClearReleases: PropTypes.func.isRequired
|
dispatchClearReleases: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
|
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
.cell {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.protocol {
|
.protocol {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.indexer {
|
.indexer {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 85px;
|
width: 85px;
|
||||||
}
|
}
|
||||||
@@ -17,7 +22,9 @@
|
|||||||
.quality,
|
.quality,
|
||||||
.customFormat,
|
.customFormat,
|
||||||
.language {
|
.language {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language {
|
.language {
|
||||||
@@ -25,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.customFormatScore {
|
.customFormatScore {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 55px;
|
width: 55px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -35,34 +42,26 @@
|
|||||||
.rejected,
|
.rejected,
|
||||||
.indexerFlags,
|
.indexerFlags,
|
||||||
.download {
|
.download {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
.size {
|
.size {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peers {
|
.peers {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
|
||||||
composes: cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title div {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history {
|
.history {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,46 +145,6 @@ class InteractiveSearchRow extends Component {
|
|||||||
{formatAge(age, ageHours, ageMinutes)}
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.download}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.rejected}>
|
|
||||||
{
|
|
||||||
!!rejections.length &&
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.DANGER}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('ReleaseRejected')}
|
|
||||||
body={
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
rejections.map((rejection, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
{rejection}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.title}>
|
<TableRowCell className={styles.title}>
|
||||||
<Link
|
<Link
|
||||||
to={infoUrl}
|
to={infoUrl}
|
||||||
@@ -297,6 +257,46 @@ class InteractiveSearchRow extends Component {
|
|||||||
}
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.rejected}>
|
||||||
|
{
|
||||||
|
!!rejections.length &&
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
name={icons.DANGER}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('ReleaseRejected')}
|
||||||
|
body={
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
rejections.map((rejection, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
{rejection}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.download}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={this.state.isConfirmGrabModalOpen}
|
isOpen={this.state.isConfirmGrabModalOpen}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
|
|
||||||
|
|
||||||
function InteractiveSearchTable(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InteractiveSearchContentConnector
|
|
||||||
searchPayload={props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InteractiveSearchTable.propTypes = {
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InteractiveSearchTable;
|
|
||||||
@@ -54,18 +54,22 @@ class DeleteMovieModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
path,
|
path,
|
||||||
hasFile,
|
statistics,
|
||||||
sizeOnDisk,
|
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
sizeOnDisk,
|
||||||
|
movieFileCount
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
const addImportExclusion = this.state.addImportExclusion;
|
const addImportExclusion = this.state.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');
|
||||||
}
|
}
|
||||||
@@ -124,9 +128,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>
|
||||||
@@ -154,10 +158,16 @@ 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,
|
statistics: PropTypes.object.isRequired,
|
||||||
sizeOnDisk: PropTypes.number.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;
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ class MovieCastPoster extends Component {
|
|||||||
|
|
||||||
const elementStyle = {
|
const elementStyle = {
|
||||||
width: `${posterWidth}px`,
|
width: `${posterWidth}px`,
|
||||||
height: `${posterHeight}px`
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentStyle = {
|
const contentStyle = {
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ class MovieCrewPoster extends Component {
|
|||||||
|
|
||||||
const elementStyle = {
|
const elementStyle = {
|
||||||
width: `${posterWidth}px`,
|
width: `${posterWidth}px`,
|
||||||
height: `${posterHeight}px`
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentStyle = {
|
const contentStyle = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
$hoverScale: 1.05;
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
border-radius: 5px;
|
||||||
transition: all 200ms ease-in;
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movie {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Navigation } from 'swiper';
|
||||||
import Measure from 'Components/Measure';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
||||||
import styles from './MovieCreditPosters.css';
|
import styles from './MovieCreditPosters.css';
|
||||||
|
|
||||||
|
// Import Swiper styles
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
// Poster container dimensions
|
// Poster container dimensions
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
@@ -169,56 +173,50 @@ class MovieCreditPosters extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items
|
items,
|
||||||
|
itemComponent
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
width,
|
posterWidth,
|
||||||
columnWidth,
|
posterHeight
|
||||||
columnCount,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const rowCount = Math.ceil(items.length / columnCount);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
<WindowScroller
|
|
||||||
scrollElement={undefined}
|
|
||||||
>
|
|
||||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
|
||||||
if (!height) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<div className={styles.sliderContainer}>
|
||||||
<div ref={registerChild}>
|
<Swiper
|
||||||
<Grid
|
slidesPerView='auto'
|
||||||
ref={this.setGridRef}
|
spaceBetween={10}
|
||||||
className={styles.grid}
|
slidesPerGroup={3}
|
||||||
autoHeight={true}
|
loop={false}
|
||||||
height={height}
|
loopFillGroupWithBlank={true}
|
||||||
columnCount={columnCount}
|
className="mySwiper"
|
||||||
columnWidth={columnWidth}
|
modules={[Navigation]}
|
||||||
rowCount={rowCount}
|
onInit={(swiper) => {
|
||||||
rowHeight={rowHeight}
|
swiper.params.navigation.prevEl = this._swiperPrevRef;
|
||||||
width={width}
|
swiper.params.navigation.nextEl = this._swiperNextRef;
|
||||||
onScroll={onChildScroll}
|
swiper.navigation.init();
|
||||||
scrollTop={scrollTop}
|
swiper.navigation.update();
|
||||||
overscanRowCount={2}
|
}}
|
||||||
cellRenderer={this.cellRenderer}
|
>
|
||||||
scrollToAlignment={'start'}
|
{items.map((credit) => (
|
||||||
isScrollingOptOut={true}
|
<SwiperSlide key={credit.tmdbId} style={{ width: posterWidth }}>
|
||||||
/>
|
<MovieCreditPosterConnector
|
||||||
</div>
|
key={credit.order}
|
||||||
);
|
component={itemComponent}
|
||||||
}
|
posterWidth={posterWidth}
|
||||||
}
|
posterHeight={posterHeight}
|
||||||
</WindowScroller>
|
tmdbId={credit.personTmdbId}
|
||||||
</Measure>
|
personName={credit.personName}
|
||||||
|
job={credit.job}
|
||||||
|
character={credit.character}
|
||||||
|
images={credit.images}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.alternateTitle {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './MovieAlternateTitles.css';
|
|
||||||
|
|
||||||
function MovieAlternateTitles({ alternateTitles }) {
|
|
||||||
return (
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={alternateTitle}
|
|
||||||
className={styles.alternateTitle}
|
|
||||||
>
|
|
||||||
{alternateTitle}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieAlternateTitles.propTypes = {
|
|
||||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieAlternateTitles;
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
.header {
|
.header {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 375px;
|
height: 425px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
@@ -39,10 +39,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.poster {
|
.poster {
|
||||||
|
z-index: 2;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 35px;
|
margin-right: 35px;
|
||||||
width: 217px;
|
width: 250px;
|
||||||
height: 319px;
|
height: 368px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import InfoLabel from 'Components/InfoLabel';
|
import InfoLabel from 'Components/InfoLabel';
|
||||||
@@ -22,12 +22,11 @@ import Popover from 'Components/Tooltip/Popover';
|
|||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
|
||||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
|
||||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||||
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
||||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||||
@@ -81,10 +80,10 @@ class MovieDetails extends Component {
|
|||||||
isEditMovieModalOpen: false,
|
isEditMovieModalOpen: false,
|
||||||
isDeleteMovieModalOpen: false,
|
isDeleteMovieModalOpen: false,
|
||||||
isInteractiveImportModalOpen: false,
|
isInteractiveImportModalOpen: false,
|
||||||
|
isInteractiveSearchModalOpen: false,
|
||||||
allExpanded: false,
|
allExpanded: false,
|
||||||
allCollapsed: false,
|
allCollapsed: false,
|
||||||
expandedState: {},
|
expandedState: {},
|
||||||
selectedTabIndex: 0,
|
|
||||||
overviewHeight: 0,
|
overviewHeight: 0,
|
||||||
titleWidth: 0
|
titleWidth: 0
|
||||||
};
|
};
|
||||||
@@ -137,6 +136,14 @@ class MovieDetails extends Component {
|
|||||||
this.setState({ isEditMovieModalOpen: false });
|
this.setState({ isEditMovieModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onInteractiveSearchPress = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveSearchModalClose = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
onDeleteMoviePress = () => {
|
onDeleteMoviePress = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditMovieModalOpen: false,
|
isEditMovieModalOpen: false,
|
||||||
@@ -265,7 +272,7 @@ class MovieDetails extends Component {
|
|||||||
ratings,
|
ratings,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
sizeOnDisk,
|
||||||
qualityProfileId,
|
qualityProfileIds,
|
||||||
monitored,
|
monitored,
|
||||||
studio,
|
studio,
|
||||||
genres,
|
genres,
|
||||||
@@ -298,9 +305,9 @@ class MovieDetails extends Component {
|
|||||||
isEditMovieModalOpen,
|
isEditMovieModalOpen,
|
||||||
isDeleteMovieModalOpen,
|
isDeleteMovieModalOpen,
|
||||||
isInteractiveImportModalOpen,
|
isInteractiveImportModalOpen,
|
||||||
|
isInteractiveSearchModalOpen,
|
||||||
overviewHeight,
|
overviewHeight,
|
||||||
titleWidth,
|
titleWidth
|
||||||
selectedTabIndex
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
||||||
@@ -326,6 +333,14 @@ class MovieDetails extends Component {
|
|||||||
onPress={onSearchPress}
|
onPress={onSearchPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('InteractiveSearch')}
|
||||||
|
iconName={icons.INTERACTIVE}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
title={undefined}
|
||||||
|
onPress={this.onInteractiveSearchPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
@@ -557,7 +572,7 @@ class MovieDetails extends Component {
|
|||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{
|
{
|
||||||
<QualityProfileNameConnector
|
<QualityProfileNameConnector
|
||||||
qualityProfileId={qualityProfileId}
|
qualityProfileIds={qualityProfileIds}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
@@ -651,101 +666,39 @@ class MovieDetails extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
|
<FieldSet legend={translate('History')}>
|
||||||
<TabList
|
<MovieHistoryTable
|
||||||
className={styles.tabList}
|
movieId={id}
|
||||||
>
|
/>
|
||||||
<Tab
|
</FieldSet>
|
||||||
className={styles.tab}
|
|
||||||
selectedClassName={styles.selectedTab}
|
|
||||||
>
|
|
||||||
{translate('History')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<FieldSet legend={translate('Files')}>
|
||||||
className={styles.tab}
|
<MovieFileEditorTable
|
||||||
selectedClassName={styles.selectedTab}
|
movieId={id}
|
||||||
>
|
/>
|
||||||
{translate('Search')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<ExtraFileTable
|
||||||
className={styles.tab}
|
movieId={id}
|
||||||
selectedClassName={styles.selectedTab}
|
/>
|
||||||
>
|
</FieldSet>
|
||||||
{translate('Files')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<FieldSet legend={translate('Cast')}>
|
||||||
className={styles.tab}
|
<MovieCastPostersConnector
|
||||||
selectedClassName={styles.selectedTab}
|
isSmallScreen={isSmallScreen}
|
||||||
>
|
/>
|
||||||
{translate('Titles')}
|
</FieldSet>
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<FieldSet legend={translate('Crew')}>
|
||||||
className={styles.tab}
|
<MovieCrewPostersConnector
|
||||||
selectedClassName={styles.selectedTab}
|
isSmallScreen={isSmallScreen}
|
||||||
>
|
/>
|
||||||
{translate('Cast')}
|
</FieldSet>
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
className={styles.tab}
|
|
||||||
selectedClassName={styles.selectedTab}
|
|
||||||
>
|
|
||||||
{translate('Crew')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
{
|
|
||||||
selectedTabIndex === 1 &&
|
|
||||||
<div className={styles.filterIcon}>
|
|
||||||
<InteractiveSearchFilterMenuConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<MovieHistoryTable
|
|
||||||
movieId={id}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<InteractiveSearchTable
|
|
||||||
movieId={id}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<MovieFileEditorTable
|
|
||||||
movieId={id}
|
|
||||||
/>
|
|
||||||
<ExtraFileTable
|
|
||||||
movieId={id}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<MovieTitlesTable
|
|
||||||
movieId={id}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<MovieCastPostersConnector
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<MovieCrewPostersConnector
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
|
<FieldSet legend={translate('TitlesAndTranslations')}>
|
||||||
|
<MovieTitlesTable
|
||||||
|
movieId={id}
|
||||||
|
/>
|
||||||
|
</FieldSet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrganizePreviewModalConnector
|
<OrganizePreviewModalConnector
|
||||||
@@ -777,6 +730,12 @@ class MovieDetails extends Component {
|
|||||||
showImportMode={false}
|
showImportMode={false}
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MovieInteractiveSearchModalConnector
|
||||||
|
isOpen={isInteractiveSearchModalOpen}
|
||||||
|
movieId={id}
|
||||||
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
|
/>
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
@@ -795,7 +754,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,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class MovieTitlesRow extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MovieTitlesRow.propTypes = {
|
MovieTitlesRow.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
language: PropTypes.object.isRequired,
|
language: PropTypes.object.isRequired,
|
||||||
sourceType: PropTypes.string.isRequired
|
sourceType: PropTypes.string.isRequired
|
||||||
|
|||||||
9
frontend/src/Movie/Details/Titles/MovieTitlesTable.css
Normal file
9
frontend/src/Movie/Details/Titles/MovieTitlesTable.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
||||||
|
import styles from './MovieTitlesTable.css';
|
||||||
|
|
||||||
function MovieTitlesTable(props) {
|
function MovieTitlesTable(props) {
|
||||||
const {
|
const {
|
||||||
@@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieTitlesTableContentConnector
|
<div className={styles.container}>
|
||||||
{...otherProps}
|
<MovieTitlesTableContentConnector
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -10,7 +9,7 @@ import styles from './MovieTitlesTableContent.css';
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
name: 'altTitle',
|
name: 'altTitle',
|
||||||
label: translate('AlternativeTitle'),
|
label: translate('Title'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,40 +31,25 @@ class MovieTitlesTableContent extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isFetching,
|
titles
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const hasItems = !!items.length;
|
const hasItems = !!titles.length;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
isFetching &&
|
!hasItems &&
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<div className={styles.blankpad}>
|
|
||||||
{translate('UnableToLoadAltTitle')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !hasItems && !error &&
|
|
||||||
<div className={styles.blankpad}>
|
<div className={styles.blankpad}>
|
||||||
{translate('NoAltTitle')}
|
{translate('NoAltTitle')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && hasItems && !error &&
|
hasItems &&
|
||||||
<Table columns={columns}>
|
<Table columns={columns}>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{
|
{
|
||||||
items.reverse().map((item) => {
|
titles.reverse().map((item) => {
|
||||||
return (
|
return (
|
||||||
<MovieTitlesRow
|
<MovieTitlesRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -83,10 +67,7 @@ class MovieTitlesTableContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MovieTitlesTableContent.propTypes = {
|
MovieTitlesTableContent.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
titles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MovieTitlesTableContent;
|
export default MovieTitlesTableContent;
|
||||||
|
|||||||
@@ -2,13 +2,40 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
import MovieTitlesTableContent from './MovieTitlesTableContent';
|
import MovieTitlesTableContent from './MovieTitlesTableContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.movies,
|
createMovieSelector(),
|
||||||
(movies) => {
|
(movie) => {
|
||||||
return movies;
|
let titles = [];
|
||||||
|
|
||||||
|
if (movie.alternateTitles) {
|
||||||
|
titles = movie.alternateTitles.map((title) => {
|
||||||
|
return {
|
||||||
|
id: `title_${title.id}`,
|
||||||
|
title: title.title,
|
||||||
|
language: title.language || 'Unknown',
|
||||||
|
sourceType: 'Alternative Title'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movie.translations) {
|
||||||
|
titles = titles.concat(movie.translations.map((title) => {
|
||||||
|
return {
|
||||||
|
id: `translation_${title.id}`,
|
||||||
|
title: title.title,
|
||||||
|
language: title.language || 'Unknown',
|
||||||
|
sourceType: 'Translation'
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
titles
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -23,14 +50,14 @@ class MovieTitlesTableContentConnector extends Component {
|
|||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const movie = this.props.items.filter((obj) => {
|
const {
|
||||||
return obj.id === this.props.movieId;
|
titles
|
||||||
});
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieTitlesTableContent
|
<MovieTitlesTableContent
|
||||||
{...this.props}
|
{...this.props}
|
||||||
items={movie[0].alternateTitles}
|
titles={titles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -38,7 +65,7 @@ class MovieTitlesTableContentConnector extends Component {
|
|||||||
|
|
||||||
MovieTitlesTableContentConnector.propTypes = {
|
MovieTitlesTableContentConnector.propTypes = {
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
titles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
|
||||||
|
|||||||
@@ -69,13 +69,15 @@ class EditMovieModalContent extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
monitored,
|
monitored,
|
||||||
qualityProfileId,
|
qualityProfileIds,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
// Id,
|
// Id,
|
||||||
path,
|
path,
|
||||||
tags
|
tags
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
|
console.log(qualityProfileIds);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
@@ -110,12 +112,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
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class MovieEditorFooter extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
monitored: NO_CHANGE,
|
monitored: NO_CHANGE,
|
||||||
qualityProfileId: NO_CHANGE,
|
qualityProfileIds: NO_CHANGE,
|
||||||
minimumAvailability: NO_CHANGE,
|
minimumAvailability: NO_CHANGE,
|
||||||
rootFolderPath: NO_CHANGE,
|
rootFolderPath: NO_CHANGE,
|
||||||
savingTags: false,
|
savingTags: false,
|
||||||
@@ -46,7 +46,7 @@ class MovieEditorFooter extends Component {
|
|||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||||
this.setState({
|
this.setState({
|
||||||
monitored: NO_CHANGE,
|
monitored: NO_CHANGE,
|
||||||
qualityProfileId: NO_CHANGE,
|
qualityProfileIds: NO_CHANGE,
|
||||||
minimumAvailability: NO_CHANGE,
|
minimumAvailability: NO_CHANGE,
|
||||||
rootFolderPath: NO_CHANGE,
|
rootFolderPath: NO_CHANGE,
|
||||||
savingTags: false
|
savingTags: false
|
||||||
@@ -143,7 +143,7 @@ class MovieEditorFooter extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
monitored,
|
monitored,
|
||||||
qualityProfileId,
|
qualityProfileIds,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
savingTags,
|
savingTags,
|
||||||
@@ -178,13 +178,13 @@ class MovieEditorFooter extends Component {
|
|||||||
|
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<MovieEditorFooterLabel
|
<MovieEditorFooterLabel
|
||||||
label={translate('QualityProfile')}
|
label={translate('QualityProfiles')}
|
||||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
isSaving={isSaving && qualityProfileIds !== NO_CHANGE}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QualityProfileSelectInputConnector
|
<QualityProfileSelectInputConnector
|
||||||
name="qualityProfileId"
|
name="qualityProfileId"
|
||||||
value={qualityProfileId}
|
value={qualityProfileIds}
|
||||||
includeNoChange={true}
|
includeNoChange={true}
|
||||||
isDisabled={!selectedCount}
|
isDisabled={!selectedCount}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
|
|||||||
9
frontend/src/Movie/History/MovieHistoryTable.css
Normal file
9
frontend/src/Movie/History/MovieHistoryTable.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
|
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
|
||||||
|
import styles from './MovieHistoryTable.css';
|
||||||
|
|
||||||
function MovieHistoryTable(props) {
|
function MovieHistoryTable(props) {
|
||||||
const {
|
const {
|
||||||
@@ -7,9 +8,11 @@ function MovieHistoryTable(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieHistoryTableContentConnector
|
<div className={styles.container}>
|
||||||
{...otherProps}
|
<MovieHistoryTableContentConnector
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,16 +21,20 @@ class MovieIndexFooter extends PureComponent {
|
|||||||
let totalFileSize = 0;
|
let totalFileSize = 0;
|
||||||
|
|
||||||
movies.forEach((s) => {
|
movies.forEach((s) => {
|
||||||
|
const { statistics = {} } = s;
|
||||||
|
|
||||||
if (s.hasFile) {
|
const {
|
||||||
movieFiles += 1;
|
movieFileCount = 0,
|
||||||
}
|
sizeOnDisk = 0
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
|
movieFiles += movieFileCount;
|
||||||
|
|
||||||
if (s.monitored) {
|
if (s.monitored) {
|
||||||
monitored++;
|
monitored++;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalFileSize += s.sizeOnDisk;
|
totalFileSize += sizeOnDisk;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,16 +14,14 @@ function createUnoptimizedSelector() {
|
|||||||
monitored,
|
monitored,
|
||||||
status,
|
status,
|
||||||
statistics,
|
statistics,
|
||||||
sizeOnDisk,
|
sizeOnDisk
|
||||||
hasFile
|
|
||||||
} = s;
|
} = s;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
monitored,
|
monitored,
|
||||||
status,
|
status,
|
||||||
statistics,
|
statistics,
|
||||||
sizeOnDisk,
|
sizeOnDisk
|
||||||
hasFile
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { createSelector } from 'reselect';
|
|||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||||
import createMovieQualityProfileSelector from 'Store/Selectors/createMovieQualityProfileSelector';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
|
|
||||||
function selectShowSearchAction() {
|
function selectShowSearchAction() {
|
||||||
@@ -29,13 +28,11 @@ function selectShowSearchAction() {
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createMovieSelector(),
|
createMovieSelector(),
|
||||||
createMovieQualityProfileSelector(),
|
|
||||||
selectShowSearchAction(),
|
selectShowSearchAction(),
|
||||||
createExecutingCommandsSelector(),
|
createExecutingCommandsSelector(),
|
||||||
(state) => state.queue.details.items,
|
(state) => state.queue.details.items,
|
||||||
(
|
(
|
||||||
movie,
|
movie,
|
||||||
qualityProfile,
|
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
executingCommands,
|
executingCommands,
|
||||||
queueItems
|
queueItems
|
||||||
@@ -68,7 +65,6 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...movie,
|
...movie,
|
||||||
qualityProfile,
|
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
isRefreshingMovie,
|
isRefreshingMovie,
|
||||||
isSearchingMovie,
|
isSearchingMovie,
|
||||||
|
|||||||
@@ -91,14 +91,14 @@ class MovieIndexOverview extends Component {
|
|||||||
title,
|
title,
|
||||||
overview,
|
overview,
|
||||||
monitored,
|
monitored,
|
||||||
hasFile,
|
|
||||||
isAvailable,
|
isAvailable,
|
||||||
status,
|
status,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
|
statistics,
|
||||||
images,
|
images,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
posterHeight,
|
posterHeight,
|
||||||
qualityProfile,
|
qualityProfileIds,
|
||||||
overviewOptions,
|
overviewOptions,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
@@ -119,6 +119,11 @@ class MovieIndexOverview extends Component {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
sizeOnDisk,
|
||||||
|
movieFileCount
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditMovieModalOpen,
|
isEditMovieModalOpen,
|
||||||
isDeleteMovieModalOpen
|
isDeleteMovieModalOpen
|
||||||
@@ -169,7 +174,7 @@ class MovieIndexOverview extends Component {
|
|||||||
|
|
||||||
<MovieIndexProgressBar
|
<MovieIndexProgressBar
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
hasFile={hasFile}
|
hasFile={movieFileCount > 0}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
status={status}
|
status={status}
|
||||||
posterWidth={posterWidth}
|
posterWidth={posterWidth}
|
||||||
@@ -248,11 +253,12 @@ class MovieIndexOverview extends Component {
|
|||||||
<MovieIndexOverviewInfo
|
<MovieIndexOverviewInfo
|
||||||
height={overviewHeight}
|
height={overviewHeight}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
qualityProfile={qualityProfile}
|
qualityProfileIds={qualityProfileIds}
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
longDateFormat={longDateFormat}
|
longDateFormat={longDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
|
sizeOnDisk={sizeOnDisk}
|
||||||
{...overviewOptions}
|
{...overviewOptions}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
@@ -282,15 +288,15 @@ MovieIndexOverview.propTypes = {
|
|||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string.isRequired,
|
overview: PropTypes.string.isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
statistics: PropTypes.object.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
posterWidth: PropTypes.number.isRequired,
|
posterWidth: PropTypes.number.isRequired,
|
||||||
posterHeight: PropTypes.number.isRequired,
|
posterHeight: PropTypes.number.isRequired,
|
||||||
rowHeight: PropTypes.number.isRequired,
|
rowHeight: PropTypes.number.isRequired,
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
overviewOptions: PropTypes.object.isRequired,
|
overviewOptions: PropTypes.object.isRequired,
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
showSearchAction: PropTypes.bool.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
@@ -312,4 +318,11 @@ MovieIndexOverview.propTypes = {
|
|||||||
queueState: PropTypes.string
|
queueState: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MovieIndexOverview.defaultProps = {
|
||||||
|
statistics: {
|
||||||
|
movieFileCount: 0,
|
||||||
|
sizeOnDisk: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default MovieIndexOverview;
|
export default MovieIndexOverview;
|
||||||
|
|||||||
@@ -79,13 +79,13 @@ function getInfoRowProps(row, props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
// if (name === 'qualityProfileId') {
|
||||||
return {
|
// return {
|
||||||
title: 'Quality Profile',
|
// title: 'Quality Profile',
|
||||||
iconName: icons.PROFILE,
|
// iconName: icons.PROFILE,
|
||||||
label: props.qualityProfile.name
|
// label: props.qualityProfile.name
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (name === 'added') {
|
if (name === 'added') {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ class MovieIndexPoster extends Component {
|
|||||||
youTubeTrailerId,
|
youTubeTrailerId,
|
||||||
title,
|
title,
|
||||||
monitored,
|
monitored,
|
||||||
hasFile,
|
|
||||||
isAvailable,
|
isAvailable,
|
||||||
status,
|
status,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
@@ -98,7 +97,7 @@ class MovieIndexPoster extends Component {
|
|||||||
showTitle,
|
showTitle,
|
||||||
showMonitored,
|
showMonitored,
|
||||||
showQualityProfile,
|
showQualityProfile,
|
||||||
qualityProfile,
|
qualityProfileIds,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
@@ -117,6 +116,7 @@ class MovieIndexPoster extends Component {
|
|||||||
onSelectedChange,
|
onSelectedChange,
|
||||||
queueStatus,
|
queueStatus,
|
||||||
queueState,
|
queueState,
|
||||||
|
statistics,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ class MovieIndexPoster extends Component {
|
|||||||
|
|
||||||
<MovieIndexProgressBar
|
<MovieIndexProgressBar
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
hasFile={hasFile}
|
hasFile={statistics.movieFileCount > 0}
|
||||||
status={status}
|
status={status}
|
||||||
posterWidth={posterWidth}
|
posterWidth={posterWidth}
|
||||||
detailedProgressBar={detailedProgressBar}
|
detailedProgressBar={detailedProgressBar}
|
||||||
@@ -265,12 +265,12 @@ class MovieIndexPoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{/* {
|
||||||
showQualityProfile &&
|
showQualityProfile &&
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
{qualityProfile.name}
|
{qualityProfile.name}
|
||||||
</div>
|
</div>
|
||||||
}
|
} */}
|
||||||
|
|
||||||
{
|
{
|
||||||
showCinemaRelease && inCinemas &&
|
showCinemaRelease && inCinemas &&
|
||||||
@@ -324,7 +324,7 @@ class MovieIndexPoster extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<MovieIndexPosterInfo
|
<MovieIndexPosterInfo
|
||||||
qualityProfile={qualityProfile}
|
qualityProfileIds={qualityProfileIds}
|
||||||
showQualityProfile={showQualityProfile}
|
showQualityProfile={showQualityProfile}
|
||||||
showReleaseDate={showReleaseDate}
|
showReleaseDate={showReleaseDate}
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
@@ -357,7 +357,6 @@ MovieIndexPoster.propTypes = {
|
|||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
@@ -368,7 +367,7 @@ MovieIndexPoster.propTypes = {
|
|||||||
showTitle: PropTypes.bool.isRequired,
|
showTitle: PropTypes.bool.isRequired,
|
||||||
showMonitored: PropTypes.bool.isRequired,
|
showMonitored: PropTypes.bool.isRequired,
|
||||||
showQualityProfile: PropTypes.bool.isRequired,
|
showQualityProfile: PropTypes.bool.isRequired,
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
showSearchAction: PropTypes.bool.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
@@ -389,7 +388,8 @@ MovieIndexPoster.propTypes = {
|
|||||||
imdbId: PropTypes.string,
|
imdbId: PropTypes.string,
|
||||||
youTubeTrailerId: PropTypes.string,
|
youTubeTrailerId: PropTypes.string,
|
||||||
queueStatus: PropTypes.string,
|
queueStatus: PropTypes.string,
|
||||||
queueState: PropTypes.string
|
queueState: PropTypes.string,
|
||||||
|
statistics: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
MovieIndexPoster.defaultProps = {
|
MovieIndexPoster.defaultProps = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.originalLanguage,
|
.originalLanguage,
|
||||||
.qualityProfileId {
|
.qualityProfileIds {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 1 0 125px;
|
flex: 1 0 125px;
|
||||||
|
|||||||
@@ -4,6 +4,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 QualityProfileListConnector from 'Components/QualityProfileListConnector';
|
||||||
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';
|
||||||
@@ -77,8 +78,9 @@ class MovieIndexRow extends Component {
|
|||||||
titleSlug,
|
titleSlug,
|
||||||
collection,
|
collection,
|
||||||
studio,
|
studio,
|
||||||
qualityProfile,
|
qualityProfileIds,
|
||||||
added,
|
added,
|
||||||
|
statistics,
|
||||||
year,
|
year,
|
||||||
inCinemas,
|
inCinemas,
|
||||||
physicalRelease,
|
physicalRelease,
|
||||||
@@ -88,7 +90,6 @@ class MovieIndexRow extends Component {
|
|||||||
runtime,
|
runtime,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
|
||||||
genres,
|
genres,
|
||||||
ratings,
|
ratings,
|
||||||
certification,
|
certification,
|
||||||
@@ -107,6 +108,10 @@ class MovieIndexRow extends Component {
|
|||||||
movieRuntimeFormat
|
movieRuntimeFormat
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
sizeOnDisk
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditMovieModalOpen,
|
isEditMovieModalOpen,
|
||||||
isDeleteMovieModalOpen
|
isDeleteMovieModalOpen
|
||||||
@@ -210,13 +215,15 @@ class MovieIndexRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
if (name === 'qualityProfileIds') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{qualityProfile.name}
|
<QualityProfileListConnector
|
||||||
|
qualityProfileIds={qualityProfileIds}
|
||||||
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -498,8 +505,9 @@ MovieIndexRow.propTypes = {
|
|||||||
originalLanguage: PropTypes.object.isRequired,
|
originalLanguage: PropTypes.object.isRequired,
|
||||||
studio: PropTypes.string,
|
studio: PropTypes.string,
|
||||||
collection: PropTypes.object,
|
collection: PropTypes.object,
|
||||||
qualityProfile: PropTypes.object.isRequired,
|
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
added: PropTypes.string,
|
added: PropTypes.string,
|
||||||
|
statistics: PropTypes.object.isRequired,
|
||||||
year: PropTypes.number,
|
year: PropTypes.number,
|
||||||
inCinemas: PropTypes.string,
|
inCinemas: PropTypes.string,
|
||||||
physicalRelease: PropTypes.string,
|
physicalRelease: PropTypes.string,
|
||||||
@@ -507,7 +515,6 @@ MovieIndexRow.propTypes = {
|
|||||||
runtime: PropTypes.number,
|
runtime: PropTypes.number,
|
||||||
minimumAvailability: PropTypes.string.isRequired,
|
minimumAvailability: PropTypes.string.isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
sizeOnDisk: PropTypes.number.isRequired,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
certification: PropTypes.string,
|
certification: PropTypes.string,
|
||||||
@@ -530,6 +537,10 @@ MovieIndexRow.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
MovieIndexRow.defaultProps = {
|
MovieIndexRow.defaultProps = {
|
||||||
|
statistics: {
|
||||||
|
movieFileCount: 0,
|
||||||
|
releaseGroups: []
|
||||||
|
},
|
||||||
genres: [],
|
genres: [],
|
||||||
tags: []
|
tags: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ function MovieFileStatus(props) {
|
|||||||
const {
|
const {
|
||||||
isAvailable,
|
isAvailable,
|
||||||
monitored,
|
monitored,
|
||||||
movieFile,
|
|
||||||
queueStatus,
|
queueStatus,
|
||||||
queueState,
|
queueState,
|
||||||
|
statistics,
|
||||||
colorImpairedMode
|
colorImpairedMode
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasMovieFile = !!movieFile;
|
const {
|
||||||
|
movieFileCount
|
||||||
|
} = statistics;
|
||||||
|
|
||||||
|
const hasMovieFile = movieFileCount > 0;
|
||||||
const hasReleased = isAvailable;
|
const hasReleased = isAvailable;
|
||||||
|
|
||||||
if (queueStatus) {
|
if (queueStatus) {
|
||||||
@@ -30,12 +34,10 @@ function MovieFileStatus(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasMovieFile) {
|
if (hasMovieFile) {
|
||||||
const quality = movieFile.quality;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.center}>
|
<div className={styles.center}>
|
||||||
<span className={styles.ended} />
|
<span className={styles.ended} />
|
||||||
{quality.quality.name}
|
Downloaded
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,10 +81,16 @@ function MovieFileStatus(props) {
|
|||||||
MovieFileStatus.propTypes = {
|
MovieFileStatus.propTypes = {
|
||||||
isAvailable: PropTypes.bool,
|
isAvailable: PropTypes.bool,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
movieFile: PropTypes.object,
|
statistics: PropTypes.object,
|
||||||
queueStatus: PropTypes.string,
|
queueStatus: PropTypes.string,
|
||||||
queueState: PropTypes.string,
|
queueState: PropTypes.string,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
colorImpairedMode: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MovieFileStatus.defaultProps = {
|
||||||
|
statistics: {
|
||||||
|
movieFileCount: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default MovieFileStatus;
|
export default MovieFileStatus;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function createMapStateToProps() {
|
|||||||
isAvailable: movie.isAvailable,
|
isAvailable: movie.isAvailable,
|
||||||
monitored: movie.monitored,
|
monitored: movie.monitored,
|
||||||
grabbed: movie.grabbed,
|
grabbed: movie.grabbed,
|
||||||
movieFile: movie.movieFile,
|
statistics: movie.statistics,
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
35
frontend/src/Movie/Search/MovieInteractiveSearchModal.js
Normal file
35
frontend/src/Movie/Search/MovieInteractiveSearchModal.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import MovieInteractiveSearchModalContent from './MovieInteractiveSearchModalContent';
|
||||||
|
|
||||||
|
function MovieInteractiveSearchModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
movieId,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
|
>
|
||||||
|
<MovieInteractiveSearchModalContent
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieInteractiveSearchModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieInteractiveSearchModal;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||||
|
import MovieInteractiveSearchModal from './MovieInteractiveSearchModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onModalClose() {
|
||||||
|
dispatch(cancelFetchReleases());
|
||||||
|
dispatch(clearReleases());
|
||||||
|
props.onModalClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(MovieInteractiveSearchModal);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
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 { scrollDirections } from 'Helpers/Props';
|
||||||
|
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||||
|
|
||||||
|
function MovieInteractiveSearchModalContent(props) {
|
||||||
|
const {
|
||||||
|
movieId,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Interactive Search
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
|
<InteractiveSearchConnector
|
||||||
|
searchPayload={{
|
||||||
|
movieId
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieInteractiveSearchModalContent.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieInteractiveSearchModalContent;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.container {
|
.container {
|
||||||
margin-top: 20px;
|
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--inputBackgroundColor);
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class GeneralSettings extends Component {
|
|||||||
packageUpdateMechanism,
|
packageUpdateMechanism,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onConfirmResetApiKey,
|
onConfirmResetApiKey,
|
||||||
|
plexServersPopulated,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -144,6 +145,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}
|
||||||
@@ -201,6 +203,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',
|
||||||
|
|||||||
@@ -5,16 +5,26 @@ 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';
|
||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const authenticationMethodOptions = [
|
export const authenticationRequiredWarning = 'To prevent remote access without authentication, Radarr now requires authentication to be enabled. You can optionally disable authentication from local addresses.';
|
||||||
{ key: 'none', value: translate('None') },
|
|
||||||
{ key: 'basic', value: translate('AuthBasic') },
|
export const authenticationMethodOptions = [
|
||||||
{ key: 'forms', value: translate('AuthForm') }
|
{ key: 'none', value: 'None', isDisabled: true },
|
||||||
|
{ key: 'basic', value: 'Basic (Browser Popup, insecure over HTTP)' },
|
||||||
|
{ key: 'forms', value: 'Forms (Login Page)' },
|
||||||
|
{ key: 'plex', value: 'Plex' },
|
||||||
|
{ key: 'oidc', value: 'OpenID Connect' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const authenticationRequiredOptions = [
|
||||||
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const certificateValidationOptions = [
|
const certificateValidationOptions = [
|
||||||
@@ -23,6 +33,22 @@ const certificateValidationOptions = [
|
|||||||
{ key: 'disabled', value: translate('Disabled') }
|
{ key: 'disabled', value: translate('Disabled') }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -62,19 +88,29 @@ class SecuritySettings extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
settings,
|
settings,
|
||||||
|
plexServersPopulated,
|
||||||
isResettingApiKey,
|
isResettingApiKey,
|
||||||
onInputChange
|
onInputChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
|
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')}>
|
||||||
@@ -86,37 +122,131 @@ class SecuritySettings extends Component {
|
|||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText')}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
helpTextWarning={authenticationRequiredWarning}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...authenticationMethod}
|
{...authenticationMethod}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Username')}</FormLabel>
|
<FormLabel>Authentication Required</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.SELECT}
|
||||||
name="username"
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText="Change which requests authentication is required for. Do not change unless you understand the risks."
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...username}
|
{...authenticationRequired}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
showUserPass &&
|
||||||
<FormGroup>
|
<>
|
||||||
<FormLabel>{translate('Password')}</FormLabel>
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.PASSWORD}
|
type={inputTypes.TEXT}
|
||||||
name="password"
|
name="username"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...password}
|
{...username}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="password"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...password}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
plexEnabled &&
|
||||||
|
<>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PlexServer')}</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>{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>
|
||||||
@@ -180,6 +310,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
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function EditImportListModalContent(props) {
|
|||||||
enableAuto,
|
enableAuto,
|
||||||
monitor,
|
monitor,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
qualityProfileId,
|
qualityProfileIds,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
searchOnAdd,
|
searchOnAdd,
|
||||||
tags,
|
tags,
|
||||||
@@ -159,8 +159,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>
|
||||||
|
|||||||
@@ -30,6 +30,6 @@
|
|||||||
code {
|
code {
|
||||||
padding: 0 1px;
|
padding: 0 1px;
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
background-color: #f7f7f7;
|
background-color: var(--pageBackground);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,26 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createQualityProfileSelector(),
|
(state, { qualityProfileIds }) => qualityProfileIds,
|
||||||
(qualityProfile) => {
|
(state) => state.settings.qualityProfiles.items,
|
||||||
|
(qualityProfileIds, allProfiles) => {
|
||||||
|
let name = 'Multiple';
|
||||||
|
|
||||||
|
if (qualityProfileIds.length === 1) {
|
||||||
|
const profile = allProfiles.find((p) => {
|
||||||
|
return p.id === qualityProfileIds[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
name = profile.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: qualityProfile.name
|
name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -24,7 +36,7 @@ function QualityProfileNameConnector({ name, ...otherProps }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileNameConnector.propTypes = {
|
QualityProfileNameConnector.propTypes = {
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
48
frontend/src/Store/Actions/Settings/plex.js
Normal file
48
frontend/src/Store/Actions/Settings/plex.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.plex';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_PLEX_RESOURCES = 'settings/plex/fetchResources';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchPlexResources = createThunk(FETCH_PLEX_RESOURCES);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
pendingChanges: {},
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
items: []
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_PLEX_RESOURCES]: createFetchHandler(section, '/authentication/plex/resources')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: { }
|
||||||
|
};
|
||||||
@@ -31,7 +31,7 @@ export const defaultState = {
|
|||||||
defaults: {
|
defaults: {
|
||||||
rootFolderPath: '',
|
rootFolderPath: '',
|
||||||
monitor: 'movieOnly',
|
monitor: 'movieOnly',
|
||||||
qualityProfileId: 0,
|
qualityProfileIds: [],
|
||||||
minimumAvailability: 'announced',
|
minimumAvailability: 'announced',
|
||||||
searchForMovie: true,
|
searchForMovie: true,
|
||||||
tags: []
|
tags: []
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export const defaultState = {
|
|||||||
|
|
||||||
options: {
|
options: {
|
||||||
showMovieInformation: true,
|
showMovieInformation: true,
|
||||||
showCutoffUnmetIcon: false
|
showCutoffUnmetIcon: false,
|
||||||
|
fullColorEvents: false
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedFilterKey: 'monitored',
|
selectedFilterKey: 'monitored',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user