1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-13 15:34:56 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Qstick
dec77f63e7 Speed up collections load by 50% 2022-11-23 21:03:52 -06:00
760 changed files with 5990 additions and 32516 deletions

View File

@@ -19,10 +19,10 @@ indent_size = 4
dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
# Indentation preferences
csharp_indent_block_contents = true
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
@@ -68,7 +64,6 @@ dotnet_diagnostic.SA1406.severity = suggestion
dotnet_diagnostic.SA1410.severity = suggestion
dotnet_diagnostic.SA1411.severity = suggestion
dotnet_diagnostic.SA1413.severity = none
dotnet_diagnostic.SA1512.severity = none
dotnet_diagnostic.SA1516.severity = none
dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1601.severity = none
@@ -167,7 +162,6 @@ dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1419.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion
@@ -183,6 +177,9 @@ dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.severity = suggestion
dotnet_diagnostic.CA1801.severity = suggestion
dotnet_diagnostic.CA1802.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion
@@ -194,11 +191,13 @@ dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.severity = suggestion
dotnet_diagnostic.CA1848.severity = suggestion
dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.severity = suggestion
dotnet_diagnostic.CA2009.severity = suggestion
dotnet_diagnostic.CA2010.severity = suggestion
dotnet_diagnostic.CA2011.severity = suggestion
dotnet_diagnostic.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion
@@ -229,9 +228,6 @@ dotnet_diagnostic.CA2243.severity = suggestion
dotnet_diagnostic.CA2244.severity = suggestion
dotnet_diagnostic.CA2245.severity = suggestion
dotnet_diagnostic.CA2246.severity = suggestion
dotnet_diagnostic.CA2249.severity = suggestion
dotnet_diagnostic.CA2251.severity = suggestion
dotnet_diagnostic.CA2254.severity = suggestion
dotnet_diagnostic.CA3061.severity = suggestion
dotnet_diagnostic.CA3075.severity = suggestion
dotnet_diagnostic.CA3076.severity = suggestion
@@ -259,7 +255,7 @@ dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion
dotnet_diagnostic.SYSLIB0006.severity = none
[*.{js,html,js,hbs,less,css}]
charset = utf-8

9
.esprintrc Normal file
View File

@@ -0,0 +1,9 @@
{
"paths": [
"frontend/src/**/*.js"
],
"ignored": [
"**/node_modules/**/*"
],
"port": 5004
}

45
.github/workflows/azuresync.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Sync issue to Azure DevOps work item
on:
issues:
types:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
concurrency: azuresync-${{ github.event.issue.number }}
permissions: {}
jobs:
alert:
permissions:
issues: write # to update issue body
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "Bug"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "User Story"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100

23
.gitignore vendored
View File

@@ -166,8 +166,27 @@ packages.config.md5sum
# Common IntelliJ Platform excludes
# Ignore Rider projects completely for now
.idea/
# User specific
**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/.idea.Radarr.Posix
**/.idea/.idea.Radarr.Windows
# Sensitive or high-churn files
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
# ignore node_modules symlink
node_modules

View File

@@ -76,15 +76,6 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrai
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
## DigitalOcean
This project is also supported by DigitalOcean
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.0.0'
majorVersion: '4.3.1'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -27,7 +27,6 @@ trigger:
include:
- develop
- master
- zeus
pr:
branches:
@@ -1093,7 +1092,7 @@ stages:
projectVersion: '$(radarrVersion)'
extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Radarr.Api.V*/**/*
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |

View File

@@ -29,7 +29,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
dotnet new tool-manifest
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V4/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v4 &
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
sleep 45

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line filenames/match-exported
const loaderUtils = require('loader-utils');
module.exports = function cssVariablesLoader(source) {

View File

@@ -56,7 +56,6 @@ class HistoryRow extends Component {
movie,
quality,
customFormats,
customFormatScore,
languages,
qualityCutoffNotMet,
eventType,
@@ -176,7 +175,7 @@ class HistoryRow extends Component {
key={name}
className={styles.customFormatScore}
>
{formatCustomFormatScore(customFormatScore)}
{formatCustomFormatScore(data.customFormatScore)}
</TableRowCell>
);
}
@@ -242,9 +241,8 @@ HistoryRow.propTypes = {
movie: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,

View File

@@ -75,23 +75,13 @@ class Queue extends Component {
return;
}
const nextState = {};
if (prevProps.items !== items) {
nextState.items = items;
}
const selectedIds = this.getSelectedIds();
const isPendingSelected = _.some(this.props.items, (item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
});
if (isPendingSelected !== this.state.isPendingSelected) {
nextState.isPendingSelected = isPendingSelected;
}
if (!_.isEmpty(nextState)) {
this.setState(nextState);
this.setState({ isPendingSelected });
}
}
@@ -224,29 +214,26 @@ class Queue extends Component {
<PageContentBody>
{
isRefreshing && !isAllPopulated ?
<LoadingIndicator /> :
null
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
}
{
!isRefreshing && hasError ?
!isRefreshing && hasError &&
<div>
{translate('FailedToLoadQueue')}
</div> :
null
</div>
}
{
isAllPopulated && !hasError && !items.length ?
isAllPopulated && !hasError && !items.length &&
<div>
{translate('QueueIsEmpty')}
</div> :
null
</div>
}
{
isAllPopulated && !hasError && !!items.length ?
isAllPopulated && !hasError && !!items.length &&
<div>
<Table
columns={columns}
@@ -281,8 +268,7 @@ class Queue extends Component {
isFetching={isRefreshing}
{...otherProps}
/>
</div> :
null
</div>
}
</PageContentBody>

View File

@@ -128,7 +128,6 @@ class QueueRow extends Component {
{
columns.map((column) => {
const {
name,
isVisible
@@ -235,16 +234,6 @@ class QueueRow extends Component {
);
}
if (name === 'year') {
return (
<TableRowCell key={name}>
{
movie ? movie.year : ''
}
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell key={name}>
@@ -373,7 +362,6 @@ QueueRow.propTypes = {
estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string,
size: PropTypes.number,
year: PropTypes.number,
sizeleft: PropTypes.number,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,

View File

@@ -20,6 +20,10 @@ class AddNewMovieModalContent extends Component {
//
// Listeners
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
};
onAddMoviePress = () => {
this.props.onAddMoviePress();
};
@@ -36,7 +40,7 @@ class AddNewMovieModalContent extends Component {
isAdding,
rootFolderPath,
monitor,
qualityProfileIds,
qualityProfileId,
minimumAvailability,
searchForMovie,
folder,
@@ -126,9 +130,9 @@ class AddNewMovieModalContent extends Component {
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
onChange={onInputChange}
{...qualityProfileIds}
name="qualityProfileId"
onChange={this.onQualityProfileIdChange}
{...qualityProfileId}
/>
</FormGroup>
@@ -185,7 +189,7 @@ AddNewMovieModalContent.propTypes = {
addError: PropTypes.object,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,

View File

@@ -58,7 +58,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId,
rootFolderPath,
monitor,
qualityProfileIds,
qualityProfileId,
minimumAvailability,
searchForMovie,
tags
@@ -68,7 +68,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileIds: qualityProfileIds.value,
qualityProfileId: qualityProfileId.value,
minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value,
tags: tags.value
@@ -93,7 +93,7 @@ AddNewMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,

View File

@@ -72,19 +72,15 @@ class AddNewMovieSearchResult extends Component {
colorImpairedMode,
id,
monitored,
hasFile,
isAvailable,
queueStatus,
queueState,
runtime,
movieRuntimeFormat,
certification,
statistics
certification
} = this.props;
const {
movieFileCount
} = statistics;
const {
isNewAddMovieModalOpen
} = this.state;
@@ -124,7 +120,7 @@ class AddNewMovieSearchResult extends Component {
isExistingMovie &&
<MovieIndexProgressBar
monitored={monitored}
hasFile={movieFileCount > 0}
hasFile={hasFile}
status={status}
posterWidth={posterWidth}
detailedProgressBar={true}
@@ -237,7 +233,7 @@ class AddNewMovieSearchResult extends Component {
{
isExistingMovie && isSmallScreen &&
<MovieStatusLabel
hasMovieFiles={movieFileCount > 0}
hasMovieFiles={hasFile}
monitored={monitored}
isAvailable={isAvailable}
id={id}
@@ -294,14 +290,7 @@ AddNewMovieSearchResult.propTypes = {
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string,
statistics: PropTypes.object
};
AddNewMovieSearchResult.defaultProps = {
statistics: {
movieFileCount: 0
}
certification: PropTypes.string
};
export default AddNewMovieSearchResult;

View File

@@ -25,13 +25,13 @@ class ImportMovieFooter extends Component {
const {
defaultMonitor,
defaultQualityProfileIds,
defaultQualityProfileId,
defaultMinimumAvailability
} = props;
this.state = {
monitor: defaultMonitor,
qualityProfileIds: defaultQualityProfileIds,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability
};
}
@@ -39,16 +39,16 @@ class ImportMovieFooter extends Component {
componentDidUpdate(prevProps, prevState) {
const {
defaultMonitor,
defaultQualityProfileIds,
defaultQualityProfileId,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdsMixed,
isQualityProfileIdMixed,
isMinimumAvailabilityMixed
} = this.props;
const {
monitor,
qualityProfileIds,
qualityProfileId,
minimumAvailability
} = this.state;
@@ -60,10 +60,10 @@ class ImportMovieFooter extends Component {
newState.monitor = defaultMonitor;
}
if (isQualityProfileIdsMixed && qualityProfileIds !== MIXED) {
newState.qualityProfileIds = MIXED;
} else if (!isQualityProfileIdsMixed && qualityProfileIds !== defaultQualityProfileIds) {
newState.qualityProfileIds = defaultQualityProfileIds;
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
newState.qualityProfileId = MIXED;
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
newState.qualityProfileId = defaultQualityProfileId;
}
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
@@ -94,7 +94,7 @@ class ImportMovieFooter extends Component {
isImporting,
isLookingUpMovie,
isMonitorMixed,
isQualityProfileIdsMixed,
isQualityProfileIdMixed,
isMinimumAvailabilityMixed,
hasUnsearchedItems,
importError,
@@ -105,7 +105,7 @@ class ImportMovieFooter extends Component {
const {
monitor,
qualityProfileIds,
qualityProfileId,
minimumAvailability
} = this.state;
@@ -148,10 +148,10 @@ class ImportMovieFooter extends Component {
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
value={qualityProfileIds}
name="qualityProfileId"
value={qualityProfileId}
isDisabled={!selectedCount}
includeMixed={isQualityProfileIdsMixed}
includeMixed={isQualityProfileIdMixed}
onChange={this.onInputChange}
/>
</div>
@@ -225,19 +225,13 @@ class ImportMovieFooter extends Component {
body={
<ul>
{
Array.isArray(importError.responseJSON) ?
importError.responseJSON.map((error, index) => {
return (
<li key={index}>
{error.errorMessage}
</li>
);
}) :
<li>
{
JSON.stringify(importError.responseJSON)
}
</li>
importError.responseJSON.map((error, index) => {
return (
<li key={index}>
{error.errorMessage}
</li>
);
})
}
</ul>
}
@@ -257,10 +251,10 @@ ImportMovieFooter.propTypes = {
isImporting: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired,
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultQualityProfileId: PropTypes.number,
defaultMinimumAvailability: PropTypes.string,
isMonitorMixed: PropTypes.bool.isRequired,
isQualityProfileIdsMixed: PropTypes.bool.isRequired,
isQualityProfileIdMixed: PropTypes.bool.isRequired,
isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
hasUnsearchedItems: PropTypes.bool.isRequired,
importError: PropTypes.object,

View File

@@ -18,7 +18,7 @@ function createMapStateToProps() {
(addMovie, importMovie, selectedIds) => {
const {
monitor: defaultMonitor,
qualityProfileIds: defaultQualityProfileIds,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability
} = addMovie.defaults;
@@ -30,7 +30,7 @@ function createMapStateToProps() {
} = importMovie;
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
const isQualityProfileIdsMixed = isMixed(items, selectedIds, defaultQualityProfileIds, 'qualityProfileIds');
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
@@ -39,10 +39,10 @@ function createMapStateToProps() {
isLookingUpMovie,
isImporting,
defaultMonitor,
defaultQualityProfileIds,
defaultQualityProfileId,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdsMixed,
isQualityProfileIdMixed,
isMinimumAvailabilityMixed,
importError,
hasUnsearchedItems

View File

@@ -11,7 +11,7 @@ function ImportMovieRow(props) {
const {
id,
monitor,
qualityProfileIds,
qualityProfileId,
minimumAvailability,
selectedMovie,
isExistingMovie,
@@ -62,8 +62,8 @@ function ImportMovieRow(props) {
<VirtualTableRowCell className={styles.qualityProfile}>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
value={qualityProfileIds}
name="qualityProfileId"
value={qualityProfileId}
onChange={onInputChange}
/>
</VirtualTableRowCell>
@@ -74,7 +74,7 @@ function ImportMovieRow(props) {
ImportMovieRow.propTypes = {
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
qualityProfileId: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired,
selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired,

View File

@@ -15,7 +15,7 @@ class ImportMovieTable extends Component {
const {
unmappedFolders,
defaultMonitor,
defaultQualityProfileIds,
defaultQualityProfileId,
defaultMinimumAvailability,
onMovieLookup,
onSetImportMovieValue
@@ -23,7 +23,7 @@ class ImportMovieTable extends Component {
const values = {
monitor: defaultMonitor,
qualityProfileIds: defaultQualityProfileIds,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability
};
@@ -167,7 +167,7 @@ ImportMovieTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultQualityProfileId: PropTypes.number,
defaultMinimumAvailability: PropTypes.string,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,

View File

@@ -13,7 +13,7 @@ function createMapStateToProps() {
(addMovie, importMovie, dimensions, allMovies) => {
return {
defaultMonitor: addMovie.defaults.monitor,
defaultQualityProfileIds: addMovie.defaults.qualityProfileIds,
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
items: importMovie.items,
isSmallScreen: dimensions.isSmallScreen,

View File

@@ -5,7 +5,6 @@ import FormInputButton from 'Components/Form/FormInputButton';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props';
@@ -243,7 +242,7 @@ class ImportMovieSelectMovie extends Component {
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
ButtonComponent={SpinnerButton}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>

View File

@@ -152,19 +152,13 @@ class ImportMovieSelectFolder extends Component {
<ul>
{
Array.isArray(saveError.responseJSON) ?
saveError.responseJSON.map((e, index) => {
return (
<li key={index}>
{e.errorMessage}
</li>
);
}) :
<li>
{
JSON.stringify(saveError.responseJSON)
}
</li>
saveError.responseJSON.map((e, index) => {
return (
<li key={index}>
{e.errorMessage}
</li>
);
})
}
</ul>
</Alert> :

View File

@@ -24,7 +24,7 @@ function createMissingMovieIdsSelector() {
const inCinemas = movie.inCinemas;
if (
(!movie.statistics || movie.statistics.movieFileCount === 0) &&
!movie.hasFile &&
moment(inCinemas).isAfter(start) &&
moment(inCinemas).isBefore(end) &&
isBefore(movie.inCinemas) &&

View File

@@ -1,5 +1,3 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.event {
overflow-x: hidden;
margin: 4px 2px;
@@ -57,10 +55,6 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.downloaded {
border-left-color: var(--successColor) !important;
&:global(.fullColor) {
background-color: rgba(39, 194, 76, 0.4) !important;
}
&:global(.colorImpaired) {
border-left-color: color(var(--successColor), saturation(+15%)) !important;
}
@@ -68,72 +62,28 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.queue {
border-left-color: var(--purple) !important;
&:global(.fullColor) {
background-color: rgba(122, 67, 182, 0.4) !important;
}
}
.unmonitored {
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 {
border-left-color: var(--warningColor) !important;
&:global(.fullColor) {
background-color: rgba(255, 165, 0, 0.6) !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);
}
}
.missingMonitored {
border-left-color: var(--dangerColor) !important;
&:global(.fullColor) {
background-color: rgba(240, 80, 80, 0.6) !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);
}
}
.unaired {
.continuing {
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);
}
}

View File

@@ -32,7 +32,6 @@ class CalendarEvent extends Component {
queueItem,
showMovieInformation,
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode,
date
} = this.props;
@@ -63,8 +62,7 @@ class CalendarEvent extends Component {
styles.event,
styles.link,
styles[statusStyle],
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
colorImpairedMode && 'colorImpaired'
)}
// component="div"
to={link}
@@ -99,7 +97,7 @@ class CalendarEvent extends Component {
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
/>
}
@@ -144,12 +142,11 @@ CalendarEvent.propTypes = {
digitalRelease: PropTypes.string,
monitored: PropTypes.bool.isRequired,
certification: PropTypes.string,
hasFile: PropTypes.bool,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired

View File

@@ -9,7 +9,6 @@ import styles from './Legend.css';
function Legend(props) {
const {
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode
} = props;
@@ -20,7 +19,7 @@ function Legend(props) {
<LegendIconItem
name={translate('CutoffUnmet')}
icon={icons.MOVIE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
kind={kinds.WARNING}
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
/>
);
@@ -32,14 +31,12 @@ function Legend(props) {
<LegendItem
style='ended'
name={translate('DownloadedAndMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='availNotMonitored'
name={translate('DownloadedButNotMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -48,14 +45,12 @@ function Legend(props) {
<LegendItem
style='missingMonitored'
name={translate('MissingMonitoredAndConsideredAvailable')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='missingUnmonitored'
name={translate('MissingNotMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -64,14 +59,12 @@ function Legend(props) {
<LegendItem
style='queue'
name={translate('Queued')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='continuing'
name={translate('Unreleased')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -86,9 +79,7 @@ function Legend(props) {
}
Legend.propTypes = {
view: PropTypes.string.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -6,12 +6,10 @@ import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.calendar.view,
createUISettingsSelector(),
(calendarOptions, view, uiSettings) => {
(calendarOptions, uiSettings) => {
return {
...calendarOptions,
view,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}

View File

@@ -8,7 +8,6 @@ function LegendIconItem(props) {
name,
icon,
kind,
darken,
tooltip
} = props;
@@ -20,7 +19,6 @@ function LegendIconItem(props) {
<Icon
className={styles.icon}
name={icon}
darken={darken}
kind={kind}
/>
@@ -33,12 +31,7 @@ LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired
};
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem;

View File

@@ -1,5 +1,3 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.legendItemContainer {
margin-right: 5px;
width: 220px;
@@ -65,12 +63,10 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.missingMonitoredColorImpaired {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
color: var(--white);
}
.missingUnmonitoredColorImpaired {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
color: var(--white);
}
.legendItemText {

View File

@@ -7,7 +7,6 @@ function LegendItem(props) {
const {
name,
style,
fullColorEvents,
colorImpairedMode
} = props;
@@ -17,8 +16,7 @@ function LegendItem(props) {
className={classNames(
styles.legendItem,
styles[style],
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
colorImpairedMode && 'colorImpaired'
)}
/>
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}>
@@ -31,7 +29,6 @@ function LegendItem(props) {
LegendItem.propTypes = {
name: PropTypes.string.isRequired,
style: PropTypes.string.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -26,16 +26,14 @@ class CalendarOptionsModalContent extends Component {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode,
fullColorEvents
enableColorImpairedMode
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode,
fullColorEvents
enableColorImpairedMode
};
}
@@ -96,7 +94,6 @@ class CalendarOptionsModalContent extends Component {
const {
showMovieInformation,
showCutoffUnmetIcon,
fullColorEvents,
onModalClose
} = this.props;
@@ -139,18 +136,6 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('FullColorEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText={translate('FullColorEventsHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
@@ -177,7 +162,7 @@ class CalendarOptionsModalContent extends Component {
values={weekColumnOptions}
value={calendarWeekColumnHeader}
onChange={this.onGlobalInputChange}
helpText={translate('SettingsWeekColumnHeaderHelpText')}
helpText={translate('HelpText')}
/>
</FormGroup>
@@ -191,9 +176,7 @@ class CalendarOptionsModalContent extends Component {
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
<FormGroup>
</FormGroup><FormGroup>
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
<FormInputGroup
@@ -204,6 +187,7 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
@@ -225,7 +209,6 @@ CalendarOptionsModalContent.propTypes = {
calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired

View File

@@ -22,7 +22,7 @@ function getUrls(state) {
tags
} = state;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v4/calendar/Radarr.ics?`;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
if (unmonitored) {
icalUrl += 'unmonitored=true&';

View File

@@ -46,7 +46,7 @@ class AddNewCollectionMovieModalContent extends Component {
onInputChange,
rootFolderPath,
monitor,
qualityProfileIds,
qualityProfileId,
minimumAvailability,
searchForMovie
} = this.props;
@@ -126,13 +126,13 @@ class AddNewCollectionMovieModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
name="qualityProfileId"
onChange={this.onQualityProfileIdChange}
{...qualityProfileIds}
{...qualityProfileId}
/>
</FormGroup>
@@ -189,7 +189,7 @@ AddNewCollectionMovieModalContent.propTypes = {
addError: PropTypes.object,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.object,
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,

View File

@@ -25,7 +25,7 @@ function createMapStateToProps() {
const collectionDefaults = {
rootFolderPath: collection.rootFolderPath,
monitor: 'movieOnly',
qualityProfileIds: collection.qualityProfileIds,
qualityProfileId: collection.qualityProfileId,
minimumAvailability: collection.minimumAvailability,
searchForMovie: collection.searchOnAdd,
tags: []
@@ -70,7 +70,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title,
rootFolderPath,
monitor,
qualityProfileIds,
qualityProfileId,
minimumAvailability,
searchForMovie,
tags
@@ -81,7 +81,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileIds: qualityProfileIds.value,
qualityProfileId: qualityProfileId.value,
minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value,
tags: tags.value
@@ -109,7 +109,7 @@ AddNewCollectionMovieModalContentConnector.propTypes = {
title: PropTypes.string.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.object,
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,

View File

@@ -46,7 +46,7 @@ class EditCollectionModalContent extends Component {
const {
monitored,
qualityProfileIds,
qualityProfileId,
minimumAvailability,
// Id,
rootFolderPath,
@@ -104,12 +104,12 @@ class EditCollectionModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
{...qualityProfileIds}
name="qualityProfileId"
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -39,7 +39,7 @@ function createMapStateToProps() {
const movieSettings = {
monitored: collection.monitored,
qualityProfileIds: collection.qualityProfileIds,
qualityProfileId: collection.qualityProfileId,
minimumAvailability: collection.minimumAvailability,
rootFolderPath: collection.rootFolderPath,
searchOnAdd: collection.searchOnAdd

View File

@@ -17,13 +17,11 @@ class CollectionMovieLabel extends Component {
status,
monitored,
isAvailable,
hasFile,
onMonitorTogglePress,
isSaving,
statistics
isSaving
} = this.props;
const { movieFileCount } = statistics;
return (
<div className={styles.movie}>
<div className={styles.movieTitle}>
@@ -48,11 +46,11 @@ class CollectionMovieLabel extends Component {
<div
className={classNames(
styles.movieStatus,
styles[getStatusStyle(status, monitored, movieFileCount > 0, isAvailable, 'kinds')]
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
)}
>
{
movieFileCount > 0 ? translate('Downloaded') : translate('Missing')
hasFile ? translate('Downloaded') : translate('Missing')
}
</div>
}
@@ -65,9 +63,9 @@ CollectionMovieLabel.propTypes = {
id: PropTypes.number,
title: PropTypes.string.isRequired,
status: PropTypes.string,
statistics: PropTypes.object.isRequired,
isAvailable: PropTypes.bool,
monitored: PropTypes.bool,
hasFile: PropTypes.bool,
isSaving: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
movieFileId: PropTypes.number,
@@ -77,7 +75,9 @@ CollectionMovieLabel.propTypes = {
CollectionMovieLabel.defaultProps = {
isSaving: false,
statistics: {
movieFileCount: 0
episodeFileCount: 0,
totalEpisodeCount: 0,
percentOfEpisodes: 0
}
};

View File

@@ -96,7 +96,7 @@ class CollectionOverview extends Component {
render() {
const {
monitored,
qualityProfileIds,
qualityProfileId,
rootFolderPath,
genres,
id,
@@ -212,7 +212,7 @@ class CollectionOverview extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileIds={qualityProfileIds}
qualityProfileId={qualityProfileId}
/>
}
</span>
@@ -325,7 +325,7 @@ class CollectionOverview extends Component {
CollectionOverview.propTypes = {
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
qualityProfileIds: PropTypes.number.isRequired,
qualityProfileId: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired,
searchOnAdd: PropTypes.bool.isRequired,
rootFolderPath: PropTypes.string.isRequired,

View File

@@ -113,12 +113,10 @@ class EnhancedSelectInput extends Component {
this._scheduleUpdate();
}
if (!Array.isArray(this.props.value)) {
if (prevProps.value !== this.props.value || prevProps.values !== this.props.values) {
this.setState({
selectedIndex: getSelectedIndex(this.props)
});
}
if (!Array.isArray(this.props.value) && prevProps.value !== this.props.value) {
this.setState({
selectedIndex: getSelectedIndex(this.props)
});
}
}
@@ -334,11 +332,6 @@ class EnhancedSelectInput extends Component {
const isMultiSelect = Array.isArray(value);
const selectedOption = getSelectedOption(selectedIndex, values);
let selectedValue = value;
if (!values.length) {
selectedValue = isMultiSelect ? [] : '';
}
return (
<div>
@@ -379,17 +372,15 @@ class EnhancedSelectInput extends Component {
onPress={this.onPress}
>
{
isFetching ?
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
/>
}
{
isFetching ?
null :
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
@@ -409,7 +400,7 @@ class EnhancedSelectInput extends Component {
onPress={this.onPress}
>
<SelectedValueComponent
value={selectedValue}
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
@@ -427,17 +418,15 @@ class EnhancedSelectInput extends Component {
>
{
isFetching ?
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
/>
}
{
isFetching ?
null :
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
@@ -517,7 +506,7 @@ class EnhancedSelectInput extends Component {
</Manager>
{
isMobile ?
isMobile &&
<Modal
className={styles.optionsModal}
size={sizes.EXTRA_SMALL}
@@ -568,8 +557,7 @@ class EnhancedSelectInput extends Component {
}
</Scroller>
</ModalBody>
</Modal> :
null
</Modal>
}
</div>
);

View File

@@ -2,19 +2,33 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import { kinds } from 'Helpers/Props';
import styles from './FormInputButton.css';
function FormInputButton(props) {
const {
className,
ButtonComponent,
canSpin,
isLastButton,
...otherProps
} = props;
if (canSpin) {
return (
<SpinnerButton
className={classNames(
className,
!isLastButton && styles.middleButton
)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
return (
<ButtonComponent
<Button
className={classNames(
className,
!isLastButton && styles.middleButton
@@ -27,14 +41,14 @@ function FormInputButton(props) {
FormInputButton.propTypes = {
className: PropTypes.string.isRequired,
ButtonComponent: PropTypes.elementType.isRequired,
isLastButton: PropTypes.bool.isRequired
isLastButton: PropTypes.bool.isRequired,
canSpin: PropTypes.bool.isRequired
};
FormInputButton.defaultProps = {
className: styles.button,
ButtonComponent: Button,
isLastButton: true
isLastButton: true,
canSpin: false
};
export default FormInputButton;

View File

@@ -6,6 +6,7 @@
.inputGroup {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
}
.inputContainer {

View File

@@ -20,7 +20,6 @@ import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector';
import PlexMachineInputConnector from './PlexMachineInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import TagInputConnector from './TagInputConnector';
@@ -63,9 +62,6 @@ function getComponent(type) {
case inputTypes.PATH:
return PathInputConnector;
case inputTypes.PLEX_MACHINE_SELECT:
return PlexMachineInputConnector;
case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector;

View File

@@ -24,7 +24,7 @@ function HintedSelectInputSelectedValue(props) {
>
<div className={styles.valueText}>
{
isMultiSelect ?
isMultiSelect &&
value.map((key, index) => {
const v = valuesMap[key];
return (
@@ -32,28 +32,26 @@ function HintedSelectInputSelectedValue(props) {
{v ? v.value : key}
</Label>
);
}) :
null
})
}
{
isMultiSelect ? null : value
!isMultiSelect && value
}
</div>
{
hint != null && includeHint ?
hint != null && includeHint &&
<div className={styles.hintText}>
{hint}
</div> :
null
</div>
}
</EnhancedSelectInputSelectedValue>
);
}
HintedSelectInputSelectedValue.propTypes = {
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
hint: PropTypes.string,
isMultiSelect: PropTypes.bool.isRequired,

View File

@@ -5,7 +5,6 @@ import { kinds } from 'Helpers/Props';
function OAuthInput(props) {
const {
className,
label,
authorizing,
error,
@@ -13,21 +12,21 @@ function OAuthInput(props) {
} = props;
return (
<SpinnerErrorButton
className={className}
kind={kinds.PRIMARY}
isSpinning={authorizing}
error={error}
onPress={onPress}
>
{label}
</SpinnerErrorButton>
<div>
<SpinnerErrorButton
kind={kinds.PRIMARY}
isSpinning={authorizing}
error={error}
onPress={onPress}
>
{label}
</SpinnerErrorButton>
</div>
);
}
OAuthInput.propTypes = {
className: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
label: PropTypes.string.isRequired,
authorizing: PropTypes.bool.isRequired,
error: PropTypes.object,
onPress: PropTypes.func.isRequired

View File

@@ -1,44 +0,0 @@
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;

View File

@@ -1,115 +0,0 @@
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);

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
@@ -45,14 +45,40 @@ function createMapStateToProps() {
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() {
return (
<EnhancedSelectInput
<SelectInput
{...this.props}
onChange={this.props.onChange}
onChange={this.onChange}
/>
);
}
@@ -60,7 +86,7 @@ class QualityProfileSelectInputConnector extends Component {
QualityProfileSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string)]),
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired

View File

@@ -68,7 +68,7 @@ RootFolderSelectInputOption.propTypes = {
value: PropTypes.string.isRequired,
freeSpace: PropTypes.number,
movieFolder: PropTypes.string,
isMissing: PropTypes.bool,
isMissing: PropTypes.boolean,
isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool
};

View File

@@ -12,10 +12,6 @@
composes: hasWarning from '~Components/Form/Input.css';
}
.hasButton {
composes: hasButton from '~Components/Form/Input.css';
}
.isDisabled {
opacity: 0.7;
cursor: not-allowed;

View File

@@ -28,7 +28,6 @@ class SelectInput extends Component {
isDisabled,
hasError,
hasWarning,
hasButton,
autoFocus,
onBlur
} = this.props;
@@ -39,7 +38,6 @@ class SelectInput extends Component {
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
hasButton && styles.hasButton,
isDisabled && disabledClassName
)}
disabled={isDisabled}
@@ -82,7 +80,6 @@ SelectInput.propTypes = {
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
hasButton: PropTypes.bool,
autoFocus: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func

View File

@@ -1,7 +1,6 @@
/* eslint-disable no-bitwise */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
import styles from './UMaskInput.css';
@@ -102,16 +101,16 @@ class UMaskInput extends Component {
</div>
<div className={styles.details}>
<div>
<label>{translate('UMask')}</label>
<label>UMask</label>
<div className={styles.value}>{umask}</div>
</div>
<div>
<label>{translate('Folder')}</label>
<label>Folder</label>
<div className={styles.value}>{folder}</div>
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
</div>
<div>
<label>{translate('File')}</label>
<label>File</label>
<div className={styles.value}>{file}</div>
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
</div>

View File

@@ -12,18 +12,10 @@
.info {
color: var(--infoColor);
&:global(.darken) {
color: color(var(--infoColor) shade(30%));
}
}
.pink {
color: var(--pink);
&:global(.darken) {
color: color(var(--pink) shade(30%));
}
}
.success {

View File

@@ -18,7 +18,6 @@ class Icon extends PureComponent {
kind,
size,
title,
darken,
isSpinning,
...otherProps
} = this.props;
@@ -27,8 +26,7 @@ class Icon extends PureComponent {
<FontAwesomeIcon
className={classNames(
className,
styles[kind],
darken && 'darken'
styles[kind]
)}
icon={name}
spin={isSpinning}
@@ -61,7 +59,6 @@ Icon.propTypes = {
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
darken: PropTypes.bool.isRequired,
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
@@ -69,7 +66,6 @@ Icon.propTypes = {
Icon.defaultProps = {
kind: kinds.DEFAULT,
size: 14,
darken: false,
isSpinning: false,
fixedWidth: false
};

View File

@@ -60,7 +60,7 @@ class FilterMenu extends Component {
iconName={icons.FILTER}
text={translate('Filter')}
isDisabled={isDisabled}
showIndicator={selectedFilterKey !== 'all'}
indicator={selectedFilterKey !== 'all'}
/>
<FilterMenuContent

View File

@@ -1,19 +1,11 @@
.menuButton {
composes: menuButton from '~./MenuButton.css';
position: relative;
&:hover {
color: #666;
}
}
.indicatorContainer {
position: absolute;
top: 10px;
left: 10px;
}
.label {
margin-left: 5px;
}

View File

@@ -1,15 +1,13 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import MenuButton from 'Components/Menu/MenuButton';
import { icons } from 'Helpers/Props';
import styles from './PageMenuButton.css';
function PageMenuButton(props) {
const {
iconName,
showIndicator,
indicator,
text,
...otherProps
} = props;
@@ -24,22 +22,6 @@ function PageMenuButton(props) {
size={18}
/>
{
showIndicator ?
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
name={icons.CIRCLE}
size={9}
/>
</span> :
null
}
<div className={styles.label}>
{text}
</div>
@@ -50,11 +32,11 @@ function PageMenuButton(props) {
PageMenuButton.propTypes = {
iconName: PropTypes.object.isRequired,
text: PropTypes.string,
showIndicator: PropTypes.bool.isRequired
indicator: PropTypes.bool.isRequired
};
PageMenuButton.defaultProps = {
showIndicator: false
indicator: false
};
export default PageMenuButton;

View File

@@ -9,7 +9,7 @@ import styles from './ToolbarMenuButton.css';
function ToolbarMenuButton(props) {
const {
iconName,
showIndicator,
indicator,
text,
...otherProps
} = props;
@@ -26,7 +26,7 @@ function ToolbarMenuButton(props) {
/>
{
showIndicator &&
indicator &&
<span
className={classNames(
styles.indicatorContainer,
@@ -53,11 +53,11 @@ function ToolbarMenuButton(props) {
ToolbarMenuButton.propTypes = {
iconName: PropTypes.object.isRequired,
text: PropTypes.string,
showIndicator: PropTypes.bool.isRequired
indicator: PropTypes.bool.isRequired
};
ToolbarMenuButton.defaultProps = {
showIndicator: false
indicator: false
};
export default ToolbarMenuButton;

View File

@@ -12,7 +12,7 @@ import styles from './PageHeaderActionsMenu.css';
function PageHeaderActionsMenu(props) {
const {
cookieAuth,
formsAuth,
onKeyboardShortcutsPress,
onRestartPress,
onShutdownPress
@@ -56,20 +56,22 @@ function PageHeaderActionsMenu(props) {
</MenuItem>
{
cookieAuth &&
<>
<div className={styles.separator} />
<MenuItem
to={`${window.Radarr.urlBase}/logout?ReturnUrl=/`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
</MenuItem>
</>
formsAuth &&
<div className={styles.separator} />
}
{
formsAuth &&
<MenuItem
to={`${window.Radarr.urlBase}/logout`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
</MenuItem>
}
</MenuContent>
</Menu>
@@ -78,7 +80,7 @@ function PageHeaderActionsMenu(props) {
}
PageHeaderActionsMenu.propTypes = {
cookieAuth: PropTypes.bool.isRequired,
formsAuth: PropTypes.bool.isRequired,
onKeyboardShortcutsPress: PropTypes.func.isRequired,
onRestartPress: PropTypes.func.isRequired,
onShutdownPress: PropTypes.func.isRequired

View File

@@ -10,7 +10,7 @@ function createMapStateToProps() {
(state) => state.system.status,
(status) => {
return {
cookieAuth: ['forms', 'oidc', 'plex'].includes(status.item.authentication)
formsAuth: status.item.authentication === 'forms'
};
}
);

View File

@@ -4,7 +4,6 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape';
import PageHeader from './Header/PageHeader';
import PageSidebar from './Sidebar/PageSidebar';
@@ -76,7 +75,6 @@ class Page extends Component {
isSmallScreen,
isSidebarVisible,
enableColorImpairedMode,
authenticationEnabled,
onSidebarToggle,
onSidebarVisibleChange
} = this.props;
@@ -111,10 +109,6 @@ class Page extends Component {
isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose}
/>
<AuthenticationRequiredModal
isOpen={!authenticationEnabled}
/>
</div>
</ColorImpairedContext.Provider>
);
@@ -130,7 +124,6 @@ Page.propTypes = {
isUpdated: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
authenticationEnabled: PropTypes.bool.isRequired,
onResize: PropTypes.func.isRequired,
onSidebarToggle: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired

View File

@@ -11,7 +11,6 @@ import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfil
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage';
import Page from './Page';
@@ -134,21 +133,18 @@ function createMapStateToProps() {
selectErrors,
selectAppProps,
createDimensionsSelector(),
createSystemStatusSelector(),
(
enableColorImpairedMode,
isPopulated,
errors,
app,
dimensions,
systemStatus
dimensions
) => {
return {
...app,
...errors,
isPopulated,
isSmallScreen: dimensions.isSmallScreen,
authenticationEnabled: systemStatus.authentication !== 'none',
enableColorImpairedMode
};
}

View File

@@ -19,7 +19,7 @@
}
}
@media only screen and (max-width: $breakpointExtraLarge) {
@media only screen and (max-width: $breakpointLarge) {
.contentFooter {
flex-wrap: wrap;
}

View File

@@ -20,11 +20,7 @@
.frontTextContainer {
z-index: 1;
color: var(--progressBarFrontTextColor);
}
.backTextContainer {
color: var(--progressBarBackTextColor);
color: var(--white);
}
.backTextContainer,

View File

@@ -1,3 +0,0 @@
.tags {
flex: 1 0 auto;
}

View File

@@ -1,38 +0,0 @@
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;

View File

@@ -1,16 +0,0 @@
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);

View File

@@ -73,7 +73,7 @@ function getInfoRowProps(row, props) {
return {
title: translate('Ratings'),
iconName: icons.HEART,
label: `${(props.ratings.tmdb.value * 10).toFixed()}%`
label: `${props.ratings.tmdb.value * 10}%`
};
}

View File

@@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModal(props) {
const {
isOpen
} = props;
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AuthenticationRequiredModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
AuthenticationRequiredModal.propTypes = {
isOpen: PropTypes.bool.isRequired
};
export default AuthenticationRequiredModal;

View File

@@ -1,5 +0,0 @@
.authRequiredAlert {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}

View File

@@ -1,267 +0,0 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup';
import 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;

View File

@@ -1,88 +0,0 @@
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);

View File

@@ -47,10 +47,6 @@ export const possibleFilterTypes = {
{ key: filterTypes.CONTAINS, value: 'contains' },
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
{ key: filterTypes.EQUAL, value: 'equal' },
{ key: filterTypes.NOT_EQUAL, value: 'not equal' },
{ key: filterTypes.STARTS_WITH, value: 'starts with' },
{ key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' },
{ key: filterTypes.ENDS_WITH, value: 'ends with' },
{ key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' }
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
]
};

View File

@@ -39,22 +39,6 @@ const filterTypePredicates = {
[filterTypes.NOT_EQUAL]: function(itemValue, filterValue) {
return itemValue !== filterValue;
},
[filterTypes.STARTS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_STARTS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.ENDS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_ENDS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
}
};

View File

@@ -10,10 +10,6 @@ export const LESS_THAN = 'lessThan';
export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
export const NOT_CONTAINS = 'notContains';
export const NOT_EQUAL = 'notEqual';
export const STARTS_WITH = 'startsWith';
export const NOT_STARTS_WITH = 'notStartsWith';
export const ENDS_WITH = 'endsWith';
export const NOT_ENDS_WITH = 'notEndsWith';
export const all = [
CONTAINS,
@@ -27,9 +23,5 @@ export const all = [
IN_LAST,
NOT_IN_LAST,
IN_NEXT,
NOT_IN_NEXT,
STARTS_WITH,
NOT_STARTS_WITH,
ENDS_WITH,
NOT_ENDS_WITH
NOT_IN_NEXT
];

View File

@@ -9,7 +9,6 @@ export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
export const PATH = 'path';
export const PLEX_MACHINE_SELECT = 'plexMachineSelect';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
@@ -36,7 +35,6 @@ export const all = [
OAUTH,
PASSWORD,
PATH,
PLEX_MACHINE_SELECT,
QUALITY_PROFILE_SELECT,
DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,

View File

@@ -64,15 +64,6 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'customFormats',
label: React.createElement(Icon, {
name: icons.INTERACTIVE,
title: translate('CustomFormat')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {

View File

@@ -5,10 +5,8 @@
}
.quality,
.languages {
.language {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
text-align: center;
}
.label {
@@ -23,7 +21,3 @@
margin-top: 0;
text-align: start;
}
.customFormatTooltip {
max-width: 250px;
}

View File

@@ -12,7 +12,6 @@ import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -151,7 +150,6 @@ class InteractiveImportRow extends Component {
languages,
releaseGroup,
size,
customFormats,
rejections,
isReprocessing,
isSelected,
@@ -228,7 +226,7 @@ class InteractiveImportRow extends Component {
</TableRowCellButton>
<TableRowCellButton
className={styles.languages}
className={styles.language}
title={translate('ClickToChangeLanguage')}
onPress={this.onSelectLanguagePress}
>
@@ -261,26 +259,7 @@ class InteractiveImportRow extends Component {
<TableRowCell>
{
customFormats?.length ?
<Popover
anchor={
<Icon name={icons.INTERACTIVE} />
}
title={translate('Formats')}
body={
<div className={styles.customFormatTooltip}>
<MovieFormats formats={customFormats} />
</div>
}
position={tooltipPositions.LEFT}
/> :
null
}
</TableRowCell>
<TableRowCell>
{
rejections.length ?
!!rejections.length &&
<Popover
anchor={
<Icon
@@ -303,9 +282,7 @@ class InteractiveImportRow extends Component {
</ul>
}
position={tooltipPositions.LEFT}
canFlip={false}
/> :
null
/>
}
</TableRowCell>
@@ -353,7 +330,6 @@ InteractiveImportRow.propTypes = {
languages: PropTypes.arrayOf(PropTypes.object),
releaseGroup: PropTypes.string,
size: PropTypes.number.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
isReprocessing: PropTypes.bool,
isSelected: PropTypes.bool,

View File

@@ -2,15 +2,12 @@ import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
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 TableBody from 'Components/Table/TableBody';
import { align, icons, sortDirections } from 'Helpers/Props';
import { icons, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearch.css';
import styles from './InteractiveSearchContent.css';
const columns = [
{
@@ -25,6 +22,20 @@ const columns = [
isSortable: 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',
label: translate('Title'),
@@ -88,24 +99,10 @@ const columns = [
label: React.createElement(Icon, { name: icons.FLAG }),
isSortable: 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 InteractiveSearch(props) {
function InteractiveSearchContent(props) {
const {
searchPayload,
isFetching,
@@ -113,63 +110,44 @@ function InteractiveSearch(props) {
error,
totalReleasesCount,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
onFilterSelect,
onGrabPress
} = props;
return (
<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 ? <LoadingIndicator /> : null
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error ?
<div>
!isFetching && !!error &&
<div className={styles.blankpad}>
{translate('UnableToLoadResultsIntSearch')}
</div> :
null
</div>
}
{
!isFetching && isPopulated && !totalReleasesCount ?
<div>
!isFetching && isPopulated && !totalReleasesCount &&
<div className={styles.blankpad}>
{translate('NoResultsFound')}
</div> :
null
</div>
}
{
!!totalReleasesCount && isPopulated && !items.length ?
<div>
!!totalReleasesCount && isPopulated && !items.length &&
<div className={styles.blankpad}>
{translate('AllResultsHiddenFilter')}
</div> :
null
</div>
}
{
isPopulated && !!items.length ?
isPopulated && !!items.length &&
<Table
columns={columns}
sortKey={sortKey}
@@ -192,38 +170,32 @@ function InteractiveSearch(props) {
})
}
</TableBody>
</Table> :
null
</Table>
}
{
totalReleasesCount !== items.length && !!items.length ?
totalReleasesCount !== items.length && !!items.length &&
<div className={styles.filteredMessage}>
{translate('SomeResultsHiddenFilter')}
</div> :
null
</div>
}
</div>
);
}
InteractiveSearch.propTypes = {
InteractiveSearchContent.propTypes = {
searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalReleasesCount: PropTypes.number.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,
sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default InteractiveSearch;
export default InteractiveSearchContent;

View File

@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearch from './InteractiveSearch';
import InteractiveSearchContent from './InteractiveSearchContent';
function createMapStateToProps(appState) {
return createSelector(
@@ -48,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
};
}
class InteractiveSearchConnector extends Component {
class InteractiveSearchContentConnector extends Component {
//
// Lifecycle
@@ -79,18 +79,18 @@ class InteractiveSearchConnector extends Component {
return (
<InteractiveSearch
<InteractiveSearchContent
{...otherProps}
/>
);
}
}
InteractiveSearchConnector.propTypes = {
InteractiveSearchContentConnector.propTypes = {
searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);

View File

@@ -1,20 +1,15 @@
.protocol {
.cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.protocol {
composes: cell;
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 {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell;
width: 85px;
}
@@ -22,9 +17,7 @@
.quality,
.customFormat,
.language {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
text-align: center;
composes: cell;
}
.language {
@@ -32,7 +25,7 @@
}
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell;
width: 55px;
font-weight: bold;
@@ -42,26 +35,34 @@
.rejected,
.indexerFlags,
.download {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell;
width: 50px;
}
.age,
.size {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell;
white-space: nowrap;
}
.peers {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell;
width: 75px;
}
.title {
composes: cell;
}
.title div {
overflow-wrap: break-word;
}
.history {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell;
width: 75px;
}

View File

@@ -145,6 +145,46 @@ class InteractiveSearchRow extends Component {
{formatAge(age, ageHours, ageMinutes)}
</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}>
<Link
to={infoUrl}
@@ -257,46 +297,6 @@ class InteractiveSearchRow extends Component {
}
</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
isOpen={this.state.isConfirmGrabModalOpen}
kind={kinds.WARNING}

View File

@@ -0,0 +1,16 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

View File

@@ -54,22 +54,18 @@ class DeleteMovieModalContent extends Component {
const {
title,
path,
statistics,
hasFile,
sizeOnDisk,
onModalClose
} = this.props;
const {
sizeOnDisk,
movieFileCount
} = statistics;
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.state.addImportExclusion;
let deleteFilesLabel = movieFileCount === 1 ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [movieFileCount]);
let deleteFilesLabel = hasFile ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [0]);
let deleteFilesHelpText = translate('DeleteFilesHelpText');
if (movieFileCount === 0) {
if (!hasFile) {
deleteFilesLabel = translate('DeleteMovieFolderLabel');
deleteFilesHelpText = translate('DeleteMovieFolderHelpText');
}
@@ -128,9 +124,9 @@ class DeleteMovieModalContent extends Component {
</div>
{
movieFileCount > 0 &&
!!hasFile &&
<div>
{movieFileCount} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
{hasFile} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
</div>
}
</div>
@@ -158,16 +154,10 @@ class DeleteMovieModalContent extends Component {
DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
hasFile: PropTypes.bool.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
DeleteMovieModalContent.defaultProps = {
statistics: {
sizeOnDisk: 0,
movieFileCount: 0
}
};
export default DeleteMovieModalContent;

View File

@@ -69,8 +69,7 @@ class MovieCastPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px'
height: `${posterHeight}px`
};
const contentStyle = {

View File

@@ -69,8 +69,7 @@ class MovieCrewPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px'
height: `${posterHeight}px`
};
const contentStyle = {

View File

@@ -1,7 +1,6 @@
$hoverScale: 1.05;
.content {
border-radius: 5px;
transition: all 200ms ease-in;
&:hover {

View File

@@ -2,10 +2,6 @@
flex: 1 0 auto;
}
.movie {
padding: 10px;
}
.container {
padding: 10px;
}

View File

@@ -1,16 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import dimensions from 'Styles/Variables/dimensions';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
@@ -173,50 +169,56 @@ class MovieCreditPosters extends Component {
render() {
const {
items,
itemComponent
items
} = this.props;
const {
posterWidth,
posterHeight
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
return (
const rowCount = Math.ceil(items.length / columnCount);
<div className={styles.sliderContainer}>
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={3}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{items.map((credit) => (
<SwiperSlide key={credit.tmdbId} style={{ width: posterWidth }}>
<MovieCreditPosterConnector
key={credit.order}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
job={credit.job}
character={credit.character}
images={credit.images}
/>
</SwiperSlide>
))}
</Swiper>
</div>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
);
}
}

View File

@@ -0,0 +1,3 @@
.alternateTitle {
white-space: nowrap;
}

View File

@@ -0,0 +1,28 @@
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;

View File

@@ -5,7 +5,7 @@
.header {
position: relative;
width: 100%;
height: 425px;
height: 375px;
}
.errorMessage {
@@ -39,11 +39,10 @@
}
.poster {
z-index: 2;
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 368px;
width: 217px;
height: 319px;
}
.info {

View File

@@ -1,8 +1,8 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel';
@@ -22,11 +22,12 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -80,10 +81,10 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false,
isInteractiveSearchModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {},
selectedTabIndex: 0,
overviewHeight: 0,
titleWidth: 0
};
@@ -136,14 +137,6 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false });
};
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
};
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditMovieModalOpen: false,
@@ -272,7 +265,7 @@ class MovieDetails extends Component {
ratings,
path,
sizeOnDisk,
qualityProfileIds,
qualityProfileId,
monitored,
studio,
genres,
@@ -305,9 +298,9 @@ class MovieDetails extends Component {
isEditMovieModalOpen,
isDeleteMovieModalOpen,
isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
overviewHeight,
titleWidth
titleWidth,
selectedTabIndex
} = this.state;
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
@@ -333,14 +326,6 @@ class MovieDetails extends Component {
onPress={onSearchPress}
/>
<PageToolbarButton
label={translate('InteractiveSearch')}
iconName={icons.INTERACTIVE}
isSpinning={isSearching}
title={undefined}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@@ -572,7 +557,7 @@ class MovieDetails extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileIds={qualityProfileIds}
qualityProfileId={qualityProfileId}
/>
}
</span>
@@ -666,39 +651,101 @@ class MovieDetails extends Component {
</div>
}
<FieldSet legend={translate('History')}>
<MovieHistoryTable
movieId={id}
/>
</FieldSet>
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
<TabList
className={styles.tabList}
>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('History')}
</Tab>
<FieldSet legend={translate('Files')}>
<MovieFileEditorTable
movieId={id}
/>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Search')}
</Tab>
<ExtraFileTable
movieId={id}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Files')}
</Tab>
<FieldSet legend={translate('Cast')}>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Titles')}
</Tab>
<FieldSet legend={translate('Crew')}>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Cast')}
</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>
<OrganizePreviewModalConnector
@@ -730,12 +777,6 @@ class MovieDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody>
</PageContent>
);
@@ -754,7 +795,7 @@ MovieDetails.propTypes = {
ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
studio: PropTypes.string,

View File

@@ -43,7 +43,7 @@ class MovieTitlesRow extends Component {
}
MovieTitlesRow.propTypes = {
id: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
sourceType: PropTypes.string.isRequired

View File

@@ -1,9 +0,0 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) {
const {
@@ -8,11 +7,9 @@ function MovieTitlesTable(props) {
} = props;
return (
<div className={styles.container}>
<MovieTitlesTableContentConnector
{...otherProps}
/>
</div>
<MovieTitlesTableContentConnector
{...otherProps}
/>
);
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import translate from 'Utilities/String/translate';
@@ -9,7 +10,7 @@ import styles from './MovieTitlesTableContent.css';
const columns = [
{
name: 'altTitle',
label: translate('Title'),
label: translate('AlternativeTitle'),
isVisible: true
},
{
@@ -31,25 +32,40 @@ class MovieTitlesTableContent extends Component {
render() {
const {
titles
isFetching,
isPopulated,
error,
items
} = this.props;
const hasItems = !!titles.length;
const hasItems = !!items.length;
return (
<div>
{
!hasItems &&
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>
{translate('UnableToLoadAltTitle')}
</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>
{translate('NoAltTitle')}
</div>
}
{
hasItems &&
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
titles.reverse().map((item) => {
items.reverse().map((item) => {
return (
<MovieTitlesRow
key={item.id}
@@ -67,7 +83,10 @@ class MovieTitlesTableContent extends Component {
}
MovieTitlesTableContent.propTypes = {
titles: PropTypes.arrayOf(PropTypes.object).isRequired
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default MovieTitlesTableContent;

View File

@@ -2,40 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import MovieTitlesTableContent from './MovieTitlesTableContent';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
(movie) => {
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
};
(state) => state.movies,
(movies) => {
return movies;
}
);
}
@@ -50,14 +23,14 @@ class MovieTitlesTableContentConnector extends Component {
// Render
render() {
const {
titles
} = this.props;
const movie = this.props.items.filter((obj) => {
return obj.id === this.props.movieId;
});
return (
<MovieTitlesTableContent
{...this.props}
titles={titles}
items={movie[0].alternateTitles}
/>
);
}
@@ -65,7 +38,7 @@ class MovieTitlesTableContentConnector extends Component {
MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
titles: PropTypes.arrayOf(PropTypes.object).isRequired
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);

View File

@@ -69,7 +69,7 @@ class EditMovieModalContent extends Component {
const {
monitored,
qualityProfileIds,
qualityProfileId,
minimumAvailability,
// Id,
path,
@@ -110,12 +110,12 @@ class EditMovieModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
{...qualityProfileIds}
name="qualityProfileId"
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -37,7 +37,7 @@ function createMapStateToProps() {
const movieSettings = {
monitored: movie.monitored,
qualityProfileIds: movie.qualityProfileIds,
qualityProfileId: movie.qualityProfileId,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags

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