mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6de0feda65 | |||
| 0f699a01f7 | |||
| be20a9d116 | |||
| 4c2fcef742 | |||
| 15a4c3b742 | |||
| 7b4f908f6d | |||
| 1b4dd405be | |||
| 135de2cad4 | |||
| 174ea347a8 | |||
| 9b4f80535e | |||
| 07b69e665d | |||
| 99441dfa67 | |||
| 8e80c85f03 | |||
| 429217d1d4 | |||
| 8257e01995 | |||
| bd3fad9636 | |||
| 3cbdba51e9 | |||
| c70ce92ee9 | |||
| c1a3a8249b | |||
| 0f93e04186 | |||
| fef666831f | |||
| 681a36e34f | |||
| 726b71027e | |||
| a8feef7e88 | |||
| 70b725a2dc | |||
| 4b3bd86e0f | |||
| 3878196f39 | |||
| a39cafe404 | |||
| d9e337f2fb | |||
| 3412e4139e | |||
| b7bacf785c | |||
| c6e3f3c26c | |||
| e4c5fc5c6e | |||
| 3c42ad0f7f | |||
| 5236d46c2b | |||
| 6f54a9e452 | |||
| 4b9107465c | |||
| 329e43c331 | |||
| f05f25af0c | |||
| e50abd276e | |||
| 933d9e074c | |||
| 993e4ca298 | |||
| 58eb24ff89 | |||
| 9516729385 | |||
| d626f0487d | |||
| 1350ccb236 | |||
| 63d05a6e78 | |||
| f60b27355b | |||
| abd63ea2a4 | |||
| 655f49b8c9 | |||
| d8c1fe5486 | |||
| 8afe4e8979 | |||
| 1935abbde2 | |||
| fdc6c66f7a | |||
| def127b93f | |||
| c75d398f14 | |||
| d4fada9b4e | |||
| 111c081545 | |||
| 7f3e7b360b | |||
| 329e37774f | |||
| 4a4037323e | |||
| 2d72c1ef34 | |||
| 337d01e4ed | |||
| 927ae86e44 | |||
| fefdd71b6d | |||
| 328850627a | |||
| f412228383 | |||
| dc82d0b6dd | |||
| 0e83c42f3a | |||
| fa80e8b7a2 | |||
| c03453f6f7 | |||
| 3ffb36a2df | |||
| 0a04fad85b | |||
| 3c7f7f2e03 | |||
| 32ec9d4872 | |||
| c8e04f0c35 | |||
| d6f849ac95 | |||
| fcea483612 | |||
| bcd87a3a30 | |||
| e3bcc3da3f | |||
| 056c2b5233 | |||
| a946546793 | |||
| f9f44aec7a | |||
| 99ff6aa9c4 | |||
| ca93a72d63 | |||
| 0c6eae256b | |||
| 508a15e09a | |||
| 180dafe696 | |||
| e3160466e0 | |||
| 9ccefe0095 | |||
| 104aadfdb7 | |||
| 8911386ed0 | |||
| 1e6540a419 | |||
| 693f8dc391 | |||
| 576e1e76af | |||
| 1f8877d192 | |||
| 8c93123126 | |||
| dd614ac005 | |||
| 82de5d6f9a | |||
| e8e54fdf99 | |||
| c3b856401e | |||
| 25f6f3ec6d | |||
| d28eb47a1a | |||
| 431bc14e76 | |||
| efe5c3beb7 | |||
| d61ce6112b | |||
| 531e948687 | |||
| 7ad4411e4d | |||
| e8e23e41dc | |||
| 0c1fc49d69 | |||
| 83632f91e6 | |||
| 1bbd08a5a0 | |||
| 298077940e | |||
| 4fb632e4fc | |||
| 7bcb492572 | |||
| a673535417 | |||
| e0d70dc341 | |||
| aa98b2bac9 | |||
| 145f67d14b | |||
| caea810908 | |||
| 9a567b93d0 | |||
| 6ecd41bc5a | |||
| d5b4f0efa9 | |||
| b337f62a34 | |||
| c42fc6094d |
@@ -1,5 +1,5 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
|
||||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
|
|||||||
@@ -3,6 +3,3 @@ contact_links:
|
|||||||
- name: Support via Discord
|
- name: Support via Discord
|
||||||
url: https://radarr.video/discord
|
url: https://radarr.video/discord
|
||||||
about: Chat with users and devs on support and setup related topics.
|
about: Chat with users and devs on support and setup related topics.
|
||||||
- name: Support via Reddit
|
|
||||||
url: https://reddit.com/r/radarr
|
|
||||||
about: Discuss and search thru support topics.
|
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ jobs:
|
|||||||
issue-comment: >
|
issue-comment: >
|
||||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||||
for bug reports and feature requests. However, this issue appears
|
for bug reports and feature requests. However, this issue appears
|
||||||
to be a support request. Please hop over onto our [Discord](https://radarr.video/discord)
|
to be a support request. Please hop over onto our [Discord](https://radarr.video/discord).
|
||||||
or [Subreddit](https://reddit.com/r/radarr)
|
|
||||||
close-issue: true
|
close-issue: true
|
||||||
|
close-reason: 'not planned'
|
||||||
lock-issue: false
|
lock-issue: false
|
||||||
- uses: dessant/support-requests@v3
|
- uses: dessant/support-requests@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ Note that only one type of a given movie is supported. If you want both an 4k ve
|
|||||||
|
|
||||||
[](https://wiki.servarr.com/radarr)
|
[](https://wiki.servarr.com/radarr)
|
||||||
[](https://radarr.video/discord)
|
[](https://radarr.video/discord)
|
||||||
[](https://www.reddit.com/r/Radarr)
|
|
||||||
|
|
||||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||||
|
|
||||||
|
|||||||
+131
-7
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '4.7.4'
|
majorVersion: '5.0.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
@@ -27,6 +27,10 @@ trigger:
|
|||||||
include:
|
include:
|
||||||
- develop
|
- develop
|
||||||
- master
|
- master
|
||||||
|
paths:
|
||||||
|
exclude:
|
||||||
|
- .github
|
||||||
|
- src/Radarr.Api.*/openapi.json
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
branches:
|
branches:
|
||||||
@@ -34,6 +38,7 @@ pr:
|
|||||||
- develop
|
- develop
|
||||||
paths:
|
paths:
|
||||||
exclude:
|
exclude:
|
||||||
|
- .github
|
||||||
- src/NzbDrone.Core/Localization/Core
|
- src/NzbDrone.Core/Localization/Core
|
||||||
- src/Radarr.Api.*/openapi.json
|
- src/Radarr.Api.*/openapi.json
|
||||||
|
|
||||||
@@ -536,8 +541,8 @@ stages:
|
|||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres
|
- job: Unit_LinuxCore_Postgres14
|
||||||
displayName: Unit Native LinuxCore with Postgres Database
|
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||||
variables:
|
variables:
|
||||||
@@ -589,7 +594,63 @@ stages:
|
|||||||
inputs:
|
inputs:
|
||||||
testResultsFormat: 'NUnit'
|
testResultsFormat: 'NUnit'
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
||||||
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
|
- job: Unit_LinuxCore_Postgres15
|
||||||
|
displayName: Unit Native LinuxCore with Postgres15 Database
|
||||||
|
dependsOn: Prepare
|
||||||
|
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||||
|
variables:
|
||||||
|
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||||
|
artifactName: linux-x64-tests
|
||||||
|
Radarr__Postgres__Host: 'localhost'
|
||||||
|
Radarr__Postgres__Port: '5432'
|
||||||
|
Radarr__Postgres__User: 'radarr'
|
||||||
|
Radarr__Postgres__Password: 'radarr'
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: UseDotNet@2
|
||||||
|
displayName: 'Install .net core'
|
||||||
|
inputs:
|
||||||
|
version: $(dotnetVersion)
|
||||||
|
- checkout: none
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Test Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: $(artifactName)
|
||||||
|
targetPath: $(testsFolder)
|
||||||
|
- bash: |
|
||||||
|
chmod a+x _tests/ffprobe
|
||||||
|
displayName: Make ffprobe Executable
|
||||||
|
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||||
|
displayName: Make Test Dummy Executable
|
||||||
|
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||||
|
- bash: |
|
||||||
|
docker run -d --name=postgres15 \
|
||||||
|
-e POSTGRES_PASSWORD=radarr \
|
||||||
|
-e POSTGRES_USER=radarr \
|
||||||
|
-p 5432:5432/tcp \
|
||||||
|
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||||
|
postgres:15
|
||||||
|
displayName: Start postgres
|
||||||
|
- bash: |
|
||||||
|
chmod a+x ${TESTSFOLDER}/test.sh
|
||||||
|
ls -lR ${TESTSFOLDER}
|
||||||
|
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||||
|
displayName: Run Tests
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
displayName: Publish Test Results
|
||||||
|
inputs:
|
||||||
|
testResultsFormat: 'NUnit'
|
||||||
|
testResultsFiles: '**/TestResult.xml'
|
||||||
|
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
- stage: Integration
|
- stage: Integration
|
||||||
@@ -675,8 +736,8 @@ stages:
|
|||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_LinuxCore_Postgres
|
- job: Integration_LinuxCore_Postgres14
|
||||||
displayName: Integration Native LinuxCore with Postgres Database
|
displayName: Integration Native LinuxCore with Postgres14 Database
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||||
variables:
|
variables:
|
||||||
@@ -733,7 +794,70 @@ stages:
|
|||||||
inputs:
|
inputs:
|
||||||
testResultsFormat: 'NUnit'
|
testResultsFormat: 'NUnit'
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
||||||
|
failTaskOnFailedTests: true
|
||||||
|
displayName: Publish Test Results
|
||||||
|
|
||||||
|
|
||||||
|
- job: Integration_LinuxCore_Postgres15
|
||||||
|
displayName: Integration Native LinuxCore with Postgres Database
|
||||||
|
dependsOn: Prepare
|
||||||
|
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||||
|
variables:
|
||||||
|
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||||
|
Radarr__Postgres__Host: 'localhost'
|
||||||
|
Radarr__Postgres__Port: '5432'
|
||||||
|
Radarr__Postgres__User: 'radarr'
|
||||||
|
Radarr__Postgres__Password: 'radarr'
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: UseDotNet@2
|
||||||
|
displayName: 'Install .net core'
|
||||||
|
inputs:
|
||||||
|
version: $(dotnetVersion)
|
||||||
|
- checkout: none
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Test Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: 'linux-x64-tests'
|
||||||
|
targetPath: $(testsFolder)
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Build Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: Packages
|
||||||
|
itemPattern: '**/$(pattern)'
|
||||||
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
|
- task: ExtractFiles@1
|
||||||
|
inputs:
|
||||||
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
|
displayName: Extract Package
|
||||||
|
- bash: |
|
||||||
|
mkdir -p ./bin/
|
||||||
|
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
|
||||||
|
displayName: Move Package Contents
|
||||||
|
- bash: |
|
||||||
|
docker run -d --name=postgres15 \
|
||||||
|
-e POSTGRES_PASSWORD=radarr \
|
||||||
|
-e POSTGRES_USER=radarr \
|
||||||
|
-p 5432:5432/tcp \
|
||||||
|
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||||
|
postgres:15
|
||||||
|
displayName: Start postgres
|
||||||
|
- bash: |
|
||||||
|
chmod a+x ${TESTSFOLDER}/test.sh
|
||||||
|
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||||
|
displayName: Run Integration Tests
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
inputs:
|
||||||
|
testResultsFormat: 'NUnit'
|
||||||
|
testResultsFiles: '**/TestResult.xml'
|
||||||
|
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function QueueDetails(props) {
|
|||||||
<Icon
|
<Icon
|
||||||
name={icons.DOWNLOAD}
|
name={icons.DOWNLOAD}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('ImportFailedInterp', [errorMessage])}
|
title={translate('ImportFailedInterp', { errorMessage })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ function QueueDetails(props) {
|
|||||||
<Icon
|
<Icon
|
||||||
name={icons.DOWNLOADING}
|
name={icons.DOWNLOADING}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DownloadFailedInterp', [errorMessage])}
|
title={translate('DownloadFailedInterp', { errorMessage })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,14 +107,14 @@ function QueueStatusCell(props) {
|
|||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||||
title = translate('DownloadWarning', [warningMessage]);
|
title = translate('DownloadWarning', { warningMessage });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOAD;
|
iconName = icons.DOWNLOAD;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('ImportFailed', [sourceTitle]);
|
title = translate('ImportFailed', { sourceTitle });
|
||||||
} else {
|
} else {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ class RemoveQueueItemModal extends Component {
|
|||||||
onModalClose={this.onModalClose}
|
onModalClose={this.onModalClose}
|
||||||
>
|
>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('Remove')} - {sourceTitle}
|
{translate('RemoveQueueItem', { sourceTitle })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
{translate('RemoveFromQueueText', [sourceTitle])}
|
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -106,7 +106,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="remove"
|
name="remove"
|
||||||
value={remove}
|
value={remove}
|
||||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||||
isDisabled={!canIgnore}
|
isDisabled={!canIgnore}
|
||||||
onChange={this.onRemoveChange}
|
onChange={this.onRemoveChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
|
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class AddNewMovie extends Component {
|
|||||||
!isFetching && !error && !items.length && !!term &&
|
!isFetching && !error && !items.length && !!term &&
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.noResults}>
|
<div className={styles.noResults}>
|
||||||
{translate('CouldNotFindResults', [term])}
|
{translate('CouldNotFindResults', { term })}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{translate('YouCanAlsoSearch')}
|
{translate('YouCanAlsoSearch')}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class ImportMovie extends Component {
|
|||||||
rootFoldersPopulated &&
|
rootFoldersPopulated &&
|
||||||
!unmappedFolders.length ?
|
!unmappedFolders.length ?
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('AllMoviesInPathHaveBeenImported', [path])}
|
{translate('AllMoviesInPathHaveBeenImported', { path })}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||||
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
@@ -43,6 +44,7 @@ export interface CustomFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import { FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
|
interface CalendarAppState extends AppSectionState<Movie> {
|
||||||
|
filterBuilderProps: FilterBuilderProp<Movie>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarAppState;
|
||||||
@@ -22,6 +22,9 @@ export interface MovieIndexAppState {
|
|||||||
showQualityProfile: boolean;
|
showQualityProfile: boolean;
|
||||||
showReleaseDate: boolean;
|
showReleaseDate: boolean;
|
||||||
showCinemaRelease: boolean;
|
showCinemaRelease: boolean;
|
||||||
|
showTmdbRating: boolean;
|
||||||
|
showImdbRating: boolean;
|
||||||
|
showRottenTomatoesRating: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||||
|
|
||||||
|
function createCalendarSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.items,
|
||||||
|
(calendar) => {
|
||||||
|
return calendar;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeriesIndexFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CalendarFilterModal(
|
||||||
|
props: SeriesIndexFilterModalProps
|
||||||
|
) {
|
||||||
|
const sectionItems = useSelector(createCalendarSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'calendar';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setCalendarFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import NoMovie from 'Movie/NoMovie';
|
|||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CalendarConnector from './CalendarConnector';
|
import CalendarConnector from './CalendarConnector';
|
||||||
|
import CalendarFilterModal from './CalendarFilterModal';
|
||||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||||
import LegendConnector from './Legend/LegendConnector';
|
import LegendConnector from './Legend/LegendConnector';
|
||||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||||
@@ -83,6 +84,7 @@ class CalendarPage extends Component {
|
|||||||
movieIsFetching,
|
movieIsFetching,
|
||||||
movieIsPopulated,
|
movieIsPopulated,
|
||||||
missingMovieIds,
|
missingMovieIds,
|
||||||
|
customFilters,
|
||||||
isRssSyncExecuting,
|
isRssSyncExecuting,
|
||||||
isSearchingForMissing,
|
isSearchingForMissing,
|
||||||
useCurrentPage,
|
useCurrentPage,
|
||||||
@@ -137,7 +139,8 @@ class CalendarPage extends Component {
|
|||||||
isDisabled={!hasMovie}
|
isDisabled={!hasMovie}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={[]}
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={CalendarFilterModal}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
@@ -208,6 +211,7 @@ CalendarPage.propTypes = {
|
|||||||
movieIsFetching: PropTypes.bool.isRequired,
|
movieIsFetching: PropTypes.bool.isRequired,
|
||||||
movieIsPopulated: PropTypes.bool.isRequired,
|
movieIsPopulated: PropTypes.bool.isRequired,
|
||||||
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||||
isSearchingForMissing: PropTypes.bool.isRequired,
|
isSearchingForMissing: PropTypes.bool.isRequired,
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as commandNames from 'Commands/commandNames';
|
|||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
|
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
|
||||||
@@ -59,6 +60,7 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.calendar.selectedFilterKey,
|
(state) => state.calendar.selectedFilterKey,
|
||||||
(state) => state.calendar.filters,
|
(state) => state.calendar.filters,
|
||||||
|
createCustomFiltersSelector('calendar'),
|
||||||
createMovieCountSelector(),
|
createMovieCountSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
createMissingMovieIdsSelector(),
|
createMissingMovieIdsSelector(),
|
||||||
@@ -67,6 +69,7 @@ function createMapStateToProps() {
|
|||||||
(
|
(
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
customFilters,
|
||||||
movieCount,
|
movieCount,
|
||||||
uiSettings,
|
uiSettings,
|
||||||
missingMovieIds,
|
missingMovieIds,
|
||||||
@@ -76,6 +79,7 @@ function createMapStateToProps() {
|
|||||||
return {
|
return {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
customFilters,
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
hasMovie: !!movieCount.count,
|
hasMovie: !!movieCount.count,
|
||||||
movieError: movieCount.error,
|
movieError: movieCount.error,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
descriptionClassName,
|
descriptionClassName,
|
||||||
title,
|
title,
|
||||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
<DescriptionListItemTitle
|
<DescriptionListItemTitle
|
||||||
className={titleClassName}
|
className={titleClassName}
|
||||||
>
|
>
|
||||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DescriptionListItem.propTypes = {
|
DescriptionListItem.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
titleClassName: PropTypes.string,
|
titleClassName: PropTypes.string,
|
||||||
descriptionClassName: PropTypes.string,
|
descriptionClassName: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import EnhancedSelectInput from './EnhancedSelectInput';
|
|||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||||
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||||
@@ -65,6 +66,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.QUALITY_PROFILE_SELECT:
|
case inputTypes.QUALITY_PROFILE_SELECT:
|
||||||
return QualityProfileSelectInputConnector;
|
return QualityProfileSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.INDEXER_SELECT:
|
||||||
|
return IndexerSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.MOVIE_MONITORED_SELECT:
|
case inputTypes.MOVIE_MONITORED_SELECT:
|
||||||
return MovieMonitoredSelectInput;
|
return MovieMonitoredSelectInput;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.indexers,
|
||||||
|
(state, { includeAny }) => includeAny,
|
||||||
|
(indexers, includeAny) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items
|
||||||
|
} = indexers;
|
||||||
|
|
||||||
|
const values = items.sort(sortByName).map((indexer) => ({
|
||||||
|
key: indexer.id,
|
||||||
|
value: indexer.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (includeAny) {
|
||||||
|
values.unshift({
|
||||||
|
key: 0,
|
||||||
|
value: '(Any)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
values
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchIndexers: fetchIndexers
|
||||||
|
};
|
||||||
|
|
||||||
|
class IndexerSelectInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.dispatchFetchIndexers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onChange = ({ name, value }) => {
|
||||||
|
this.props.onChange({ name, value: parseInt(value) });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...this.props}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexerSelectInputConnector.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
includeAny: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
IndexerSelectInputConnector.defaultProps = {
|
||||||
|
includeAny: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
||||||
@@ -41,7 +41,7 @@ class NumberInput extends Component {
|
|||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { value } = this.props;
|
const { value } = this.props;
|
||||||
|
|
||||||
if (value !== prevProps.value && !this.state.isFocused) {
|
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
|
||||||
this.setState({
|
this.setState({
|
||||||
value: value == null ? '' : value.toString()
|
value: value == null ? '' : value.toString()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ class TextTagInputConnector extends Component {
|
|||||||
// to oddities with restrictions (as an example).
|
// to oddities with restrictions (as an example).
|
||||||
|
|
||||||
const newValue = [...valueArray];
|
const newValue = [...valueArray];
|
||||||
const newTags = split(tag.name);
|
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
|
||||||
|
|
||||||
newTags.forEach((newTag) => {
|
newTags.forEach((newTag) => {
|
||||||
newValue.push(newTag.trim());
|
newValue.push(newTag.trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
onChange({ name, value: newValue.join(',') });
|
onChange({ name, value: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
onTagDelete = ({ index }) => {
|
onTagDelete = ({ index }) => {
|
||||||
@@ -67,7 +67,7 @@ class TextTagInputConnector extends Component {
|
|||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
name,
|
name,
|
||||||
value: newValue.join(',')
|
value: newValue
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import Link from './Link';
|
import Link from './Link';
|
||||||
import styles from './IconButton.css';
|
import styles from './IconButton.css';
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ function IconButton(props) {
|
|||||||
className,
|
className,
|
||||||
isDisabled && styles.isDisabled
|
isDisabled && styles.isDisabled
|
||||||
)}
|
)}
|
||||||
aria-label="Table Options Button"
|
aria-label={translate('TableOptionsButton')}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
kind,
|
||||||
isSpinning,
|
isSpinning,
|
||||||
error,
|
error,
|
||||||
children,
|
children,
|
||||||
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
|
|||||||
const showIcon = wasSuccessful || hasWarning || hasError;
|
const showIcon = wasSuccessful || hasWarning || hasError;
|
||||||
|
|
||||||
let iconName = icons.CHECK;
|
let iconName = icons.CHECK;
|
||||||
let iconKind = kinds.SUCCESS;
|
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
|
||||||
|
|
||||||
if (hasWarning) {
|
if (hasWarning) {
|
||||||
iconName = icons.WARNING;
|
iconName = icons.WARNING;
|
||||||
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
|
kind={kind}
|
||||||
isSpinning={isSpinning}
|
isSpinning={isSpinning}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SpinnerErrorButton.propTypes = {
|
SpinnerErrorButton.propTypes = {
|
||||||
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
isSpinning: PropTypes.bool.isRequired,
|
isSpinning: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
children: PropTypes.node.isRequired
|
children: PropTypes.node.isRequired
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
|||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import PageHeader from './Header/PageHeader';
|
import PageHeader from './Header/PageHeader';
|
||||||
import PageSidebar from './Sidebar/PageSidebar';
|
import PageSidebar from './Sidebar/PageSidebar';
|
||||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
|||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
|
authenticationEnabled,
|
||||||
onSidebarToggle,
|
onSidebarToggle,
|
||||||
onSidebarVisibleChange
|
onSidebarVisibleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -109,6 +111,10 @@ class Page extends Component {
|
|||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AuthenticationRequiredModal
|
||||||
|
isOpen={!authenticationEnabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ColorImpairedContext.Provider>
|
</ColorImpairedContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -124,6 +130,7 @@ Page.propTypes = {
|
|||||||
isUpdated: PropTypes.bool.isRequired,
|
isUpdated: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
authenticationEnabled: PropTypes.bool.isRequired,
|
||||||
onResize: PropTypes.func.isRequired,
|
onResize: PropTypes.func.isRequired,
|
||||||
onSidebarToggle: PropTypes.func.isRequired,
|
onSidebarToggle: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfil
|
|||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
@@ -140,18 +141,21 @@ function createMapStateToProps() {
|
|||||||
selectErrors,
|
selectErrors,
|
||||||
selectAppProps,
|
selectAppProps,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
(
|
(
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
errors,
|
errors,
|
||||||
app,
|
app,
|
||||||
dimensions
|
dimensions,
|
||||||
|
systemStatus
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
...errors,
|
...errors,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class DiscoverMovieFooter extends Component {
|
|||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<div className={styles.buttonContainerContent}>
|
<div className={styles.buttonContainerContent}>
|
||||||
<DiscoverMovieFooterLabel
|
<DiscoverMovieFooterLabel
|
||||||
label={translate('MoviesSelectedInterp', [selectedCount])}
|
label={translate('MoviesSelectedInterp', { count: selectedCount })}
|
||||||
isSaving={false}
|
isSaving={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function NoDiscoverMovie(props) {
|
|||||||
to="/settings/importlists"
|
to="/settings/importlists"
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
>
|
>
|
||||||
{translate('AddList')}
|
{translate('AddImportList')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
enum DownloadProtocol {
|
||||||
|
Unknown = 'unknown',
|
||||||
|
Usenet = 'usenet',
|
||||||
|
Torrent = 'torrent',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadProtocol;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||||
|
|
||||||
|
function onModalClose() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AuthenticationRequiredModalContentConnector
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModal;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.authRequiredAlert {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'addCustomFormatMessage': string;
|
'authRequiredAlert': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AuthenticationRequiredModalContent.css';
|
||||||
|
|
||||||
|
function onModalClose() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthenticationRequiredModalContent(props) {
|
||||||
|
const {
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
settings,
|
||||||
|
onInputChange,
|
||||||
|
onSavePress,
|
||||||
|
dispatchFetchStatus
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||||
|
|
||||||
|
const didMount = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSaving && didMount.current) {
|
||||||
|
dispatchFetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
didMount.current = true;
|
||||||
|
}, [isSaving, dispatchFetchStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent
|
||||||
|
showCloseButton={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('AuthenticationRequired')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Alert
|
||||||
|
className={styles.authRequiredAlert}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
>
|
||||||
|
{translate('AuthenticationRequiredWarning')}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error ?
|
||||||
|
<div>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationMethod"
|
||||||
|
values={authenticationMethodOptions}
|
||||||
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationMethod}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText={translate('AuthenticationRequiredHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationRequired}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="username"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...username}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="password"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...password}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!authenticationEnabled}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationRequiredModalContent.propTypes = {
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
settings: PropTypes.object.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthenticationRequiredModalContent;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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),
|
||||||
|
(sectionSettings) => {
|
||||||
|
return {
|
||||||
|
...sectionSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
import * as filterTypes from './filterTypes';
|
import * as filterTypes from './filterTypes';
|
||||||
|
|
||||||
export const ARRAY = 'array';
|
export const ARRAY = 'array';
|
||||||
|
export const CONTAINS = 'contains';
|
||||||
export const DATE = 'date';
|
export const DATE = 'date';
|
||||||
|
export const EQUAL = 'equal';
|
||||||
export const EXACT = 'exact';
|
export const EXACT = 'exact';
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const STRING = 'string';
|
export const STRING = 'string';
|
||||||
|
|
||||||
export const all = [
|
export const all = [
|
||||||
ARRAY,
|
ARRAY,
|
||||||
|
CONTAINS,
|
||||||
DATE,
|
DATE,
|
||||||
|
EQUAL,
|
||||||
EXACT,
|
EXACT,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
STRING
|
STRING
|
||||||
@@ -20,6 +24,10 @@ export const possibleFilterTypes = {
|
|||||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
[CONTAINS]: [
|
||||||
|
{ key: filterTypes.CONTAINS, value: 'contains' }
|
||||||
|
],
|
||||||
|
|
||||||
[DATE]: [
|
[DATE]: [
|
||||||
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
||||||
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
||||||
@@ -29,6 +37,10 @@ export const possibleFilterTypes = {
|
|||||||
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
[EQUAL]: [
|
||||||
|
{ key: filterTypes.EQUAL, value: 'is' }
|
||||||
|
],
|
||||||
|
|
||||||
[EXACT]: [
|
[EXACT]: [
|
||||||
{ key: filterTypes.EQUAL, value: 'is' },
|
{ key: filterTypes.EQUAL, value: 'is' },
|
||||||
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
faChevronCircleRight as fasChevronCircleRight,
|
faChevronCircleRight as fasChevronCircleRight,
|
||||||
faChevronCircleUp as fasChevronCircleUp,
|
faChevronCircleUp as fasChevronCircleUp,
|
||||||
faCircle as fasCircle,
|
faCircle as fasCircle,
|
||||||
|
faCircleDown as fasCircleDown,
|
||||||
faCloud as fasCloud,
|
faCloud as fasCloud,
|
||||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||||
faCog as fasCog,
|
faCog as fasCog,
|
||||||
@@ -135,6 +136,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
|||||||
export const CHECK_CIRCLE = fasCheckCircle;
|
export const CHECK_CIRCLE = fasCheckCircle;
|
||||||
export const CHECK_SQUARE = fasSquareCheck;
|
export const CHECK_SQUARE = fasSquareCheck;
|
||||||
export const CIRCLE = fasCircle;
|
export const CIRCLE = fasCircle;
|
||||||
|
export const CIRCLE_DOWN = fasCircleDown;
|
||||||
export const CIRCLE_OUTLINE = farCircle;
|
export const CIRCLE_OUTLINE = farCircle;
|
||||||
export const CLEAR = fasTrashAlt;
|
export const CLEAR = fasTrashAlt;
|
||||||
export const CLIPBOARD = fasCopy;
|
export const CLIPBOARD = fasCopy;
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ export const CHECK = 'check';
|
|||||||
export const DEVICE = 'device';
|
export const DEVICE = 'device';
|
||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||||
|
export const FLOAT = 'float';
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const OAUTH = 'oauth';
|
export const OAUTH = 'oauth';
|
||||||
export const PASSWORD = 'password';
|
export const PASSWORD = 'password';
|
||||||
export const PATH = 'path';
|
export const PATH = 'path';
|
||||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||||
|
export const INDEXER_SELECT = 'indexerSelect';
|
||||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||||
export const LANGUAGE_SELECT = 'languageSelect';
|
export const LANGUAGE_SELECT = 'languageSelect';
|
||||||
@@ -31,11 +33,13 @@ export const all = [
|
|||||||
DEVICE,
|
DEVICE,
|
||||||
KEY_VALUE_LIST,
|
KEY_VALUE_LIST,
|
||||||
MOVIE_MONITORED_SELECT,
|
MOVIE_MONITORED_SELECT,
|
||||||
|
FLOAT,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
OAUTH,
|
OAUTH,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
PATH,
|
PATH,
|
||||||
QUALITY_PROFILE_SELECT,
|
QUALITY_PROFILE_SELECT,
|
||||||
|
INDEXER_SELECT,
|
||||||
DOWNLOAD_CLIENT_SELECT,
|
DOWNLOAD_CLIENT_SELECT,
|
||||||
ROOT_FOLDER_SELECT,
|
ROOT_FOLDER_SELECT,
|
||||||
INDEXER_FLAGS_SELECT,
|
INDEXER_FLAGS_SELECT,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function InteractiveImportSelectFolderModalContent(
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{modalTitle} - {translate('SelectFolder')}
|
{translate('SelectFolderModalTitle', { modalTitle })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ const COLUMNS = [
|
|||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
name: icons.DANGER,
|
name: icons.DANGER,
|
||||||
kind: kinds.DANGER,
|
kind: kinds.DANGER,
|
||||||
|
title: () => translate('Rejections'),
|
||||||
}),
|
}),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
@@ -242,10 +243,23 @@ function InteractiveImportModalContent(
|
|||||||
useState<string | null>(null);
|
useState<string | null>(null);
|
||||||
const [selectState, setSelectState] = useSelectState();
|
const [selectState, setSelectState] = useSelectState();
|
||||||
const [bulkSelectOptions, setBulkSelectOptions] = useState([
|
const [bulkSelectOptions, setBulkSelectOptions] = useState([
|
||||||
{ key: 'select', value: translate('SelectDotDot'), disabled: true },
|
{
|
||||||
{ key: 'quality', value: translate('SelectQuality') },
|
key: 'select',
|
||||||
{ key: 'releaseGroup', value: translate('SelectReleaseGroup') },
|
value: translate('SelectDropdown'),
|
||||||
{ key: 'language', value: translate('SelectLanguage') },
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'quality',
|
||||||
|
value: translate('SelectQuality'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'releaseGroup',
|
||||||
|
value: translate('SelectReleaseGroup'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'language',
|
||||||
|
value: translate('SelectLanguage'),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
const { allSelected, allUnselected, selectedState } = selectState;
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
const previousIsDeleting = usePrevious(isDeleting);
|
const previousIsDeleting = usePrevious(isDeleting);
|
||||||
@@ -390,7 +404,9 @@ function InteractiveImportModalContent(
|
|||||||
const files: InteractiveImportCommandOptions[] = [];
|
const files: InteractiveImportCommandOptions[] = [];
|
||||||
|
|
||||||
if (finalImportMode === 'chooseImportMode') {
|
if (finalImportMode === 'chooseImportMode') {
|
||||||
setInteractiveImportErrorMessage('An import mode must be selected');
|
setInteractiveImportErrorMessage(
|
||||||
|
translate('InteractiveImportNoImportMode')
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -403,21 +419,21 @@ function InteractiveImportModalContent(
|
|||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
setInteractiveImportErrorMessage(
|
setInteractiveImportErrorMessage(
|
||||||
translate('InteractiveImportErrMovie')
|
translate('InteractiveImportNoMovie')
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!quality) {
|
if (!quality) {
|
||||||
setInteractiveImportErrorMessage(
|
setInteractiveImportErrorMessage(
|
||||||
translate('InteractiveImportErrQuality')
|
translate('InteractiveImportNoQuality')
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!languages) {
|
if (!languages) {
|
||||||
setInteractiveImportErrorMessage(
|
setInteractiveImportErrorMessage(
|
||||||
translate('InteractiveImportErrLanguage')
|
translate('InteractiveImportNoLanguage')
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -605,7 +621,7 @@ function InteractiveImportModalContent(
|
|||||||
|
|
||||||
const errorMessage = getErrorMessage(
|
const errorMessage = getErrorMessage(
|
||||||
error,
|
error,
|
||||||
translate('UnableToLoadManualImportItems')
|
translate('InteractiveImportLoadError')
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -685,7 +701,7 @@ function InteractiveImportModalContent(
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isPopulated && !items.length && !isFetching
|
{isPopulated && !items.length && !isFetching
|
||||||
? translate('NoVideoFilesFoundSelectedFolder')
|
? translate('InteractiveImportNoFilesFound')
|
||||||
: null}
|
: null}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
@@ -781,8 +797,8 @@ function InteractiveImportModalContent(
|
|||||||
isOpen={isConfirmDeleteModalOpen}
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteSelectedMovieFiles')}
|
title={translate('DeleteSelectedMovieFiles')}
|
||||||
message={translate('DeleteSelectedMovieFilesMessage')}
|
message={translate('DeleteSelectedMovieFilesHelpText')}
|
||||||
confirmLabel="Delete"
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={onConfirmDelete}
|
onConfirm={onConfirmDelete}
|
||||||
onCancel={onConfirmDeleteModalClose}
|
onCancel={onConfirmDeleteModalClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -80,16 +80,14 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{modalTitle} - {translate('SelectLanguage')}
|
{translate('SelectLanguageModalTitle', { modalTitle })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
{!isFetching && error ? (
|
{!isFetching && error ? (
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>{translate('LanguagesLoadError')}</Alert>
|
||||||
{translate('UnableToLoadLanguages')}
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isPopulated && !error ? (
|
{isPopulated && !error ? (
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'imdbId',
|
name: 'imdbId',
|
||||||
label: () => translate('ImdbId'),
|
label: () => translate('IMDbId'),
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tmdbId',
|
name: 'tmdbId',
|
||||||
label: () => translate('TmdbId'),
|
label: () => translate('TMDBId'),
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -166,8 +166,11 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
|||||||
a.sortTitle.localeCompare(b.sortTitle)
|
a.sortTitle.localeCompare(b.sortTitle)
|
||||||
);
|
);
|
||||||
|
|
||||||
return sorted.filter((item) =>
|
return sorted.filter(
|
||||||
item.title.toLowerCase().includes(filter.toLowerCase())
|
(item) =>
|
||||||
|
item.title.toLowerCase().includes(filter.toLowerCase()) ||
|
||||||
|
item.tmdbId.toString().includes(filter) ||
|
||||||
|
item.imdbId?.includes(filter)
|
||||||
);
|
);
|
||||||
}, [allMovies, filter]);
|
}, [allMovies, filter]);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ class SelectMovieRow extends Component {
|
|||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.imdbId}>
|
<VirtualTableRowCell className={styles.imdbId}>
|
||||||
<Label>{this.props.imdbId}</Label>
|
{
|
||||||
|
this.props.imdbId ?
|
||||||
|
<Label>{this.props.imdbId}</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.tmdbId}>
|
<VirtualTableRowCell className={styles.tmdbId}>
|
||||||
@@ -43,7 +47,7 @@ SelectMovieRow.propTypes = {
|
|||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
tmdbId: PropTypes.number.isRequired,
|
tmdbId: PropTypes.number.isRequired,
|
||||||
imdbId: PropTypes.string.isRequired,
|
imdbId: PropTypes.string,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
onMovieSelect: PropTypes.func.isRequired
|
onMovieSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,9 +131,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
|
|||||||
{isFetching && <LoadingIndicator />}
|
{isFetching && <LoadingIndicator />}
|
||||||
|
|
||||||
{!isFetching && error ? (
|
{!isFetching && error ? (
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>{translate('QualitiesLoadError')}</Alert>
|
||||||
{translate('UnableToLoadQualities')}
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isPopulated && !error ? (
|
{isPopulated && !error ? (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function SelectReleaseGroupModalContent(
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{modalTitle} - {translate('SetReleaseGroup')}
|
{translate('SetReleaseGroupModalTitle', { modalTitle })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody
|
<ModalBody
|
||||||
@@ -62,7 +62,7 @@ function SelectReleaseGroupModalContent(
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>Cancel</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button kind={kinds.SUCCESS} onPress={onReleaseGroupSelectWrapper}>
|
<Button kind={kinds.SUCCESS} onPress={onReleaseGroupSelectWrapper}>
|
||||||
{translate('SetReleaseGroup')}
|
{translate('SetReleaseGroup')}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { icons, kinds, sortDirections } from 'Helpers/Props';
|
import { icons, kinds, sortDirections } from 'Helpers/Props';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
||||||
import styles from './InteractiveSearchContent.css';
|
import styles from './InteractiveSearchContent.css';
|
||||||
@@ -32,7 +33,11 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'rejections',
|
name: 'rejections',
|
||||||
label: React.createElement(Icon, { name: icons.DANGER }),
|
columnLabel: () => translate('Rejections'),
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.DANGER,
|
||||||
|
title: () => translate('Rejections')
|
||||||
|
}),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
fixedSortDirection: sortDirections.ASCENDING,
|
fixedSortDirection: sortDirections.ASCENDING,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
@@ -88,6 +93,7 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'customFormatScore',
|
name: 'customFormatScore',
|
||||||
|
columnLabel: () => translate('CustomFormatScore'),
|
||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
name: icons.SCORE,
|
name: icons.SCORE,
|
||||||
title: () => translate('CustomFormatScore')
|
title: () => translate('CustomFormatScore')
|
||||||
@@ -97,7 +103,11 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'indexerFlags',
|
name: 'indexerFlags',
|
||||||
label: React.createElement(Icon, { name: icons.FLAG }),
|
columnLabel: () => translate('IndexerFlags'),
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.FLAG,
|
||||||
|
title: () => translate('IndexerFlags')
|
||||||
|
}),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
}
|
}
|
||||||
@@ -119,36 +129,46 @@ function InteractiveSearchContent(props) {
|
|||||||
onGrabPress
|
onGrabPress
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
isFetching &&
|
isFetching ? <LoadingIndicator /> : null
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && error ?
|
||||||
<Alert kind={kinds.DANGER} className={styles.alert}>
|
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||||
{translate('UnableToLoadResultsIntSearch')}
|
{
|
||||||
</Alert>
|
errorMessage ?
|
||||||
|
<Fragment>
|
||||||
|
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
|
||||||
|
</Fragment> :
|
||||||
|
translate('MovieSearchResultsLoadError')
|
||||||
|
}
|
||||||
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && isPopulated && !totalReleasesCount &&
|
!isFetching && isPopulated && !totalReleasesCount ?
|
||||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||||
{translate('NoResultsFound')}
|
{translate('NoResultsFound')}
|
||||||
</Alert>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!totalReleasesCount && isPopulated && !items.length &&
|
!!totalReleasesCount && isPopulated && !items.length ?
|
||||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||||
{translate('AllResultsHiddenFilter')}
|
{translate('AllResultsHiddenFilter')}
|
||||||
</Alert>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !!items.length &&
|
isPopulated && !!items.length ?
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
@@ -171,14 +191,16 @@ function InteractiveSearchContent(props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
totalReleasesCount !== items.length && !!items.length &&
|
totalReleasesCount !== items.length && !!items.length ?
|
||||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||||
{translate('SomeResultsHiddenFilter')}
|
{translate('SomeResultsHiddenFilter')}
|
||||||
</Alert>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
|
|
||||||
.quality,
|
.quality,
|
||||||
.customFormat,
|
.customFormat,
|
||||||
.language {
|
.languages {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language {
|
.languages {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rejected,
|
.rejected,
|
||||||
.indexerFlags,
|
.indexerFlags {
|
||||||
.download {
|
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
width: 50px;
|
width: 50px;
|
||||||
@@ -70,3 +69,39 @@
|
|||||||
.blocklist {
|
.blocklist {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download {
|
||||||
|
composes: cell;
|
||||||
|
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualDownloadContent {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
height: 20.39px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 20.39px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--iconButtonHoverColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactiveIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 0;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 8px;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ interface CssExports {
|
|||||||
'customFormat': string;
|
'customFormat': string;
|
||||||
'customFormatScore': string;
|
'customFormatScore': string;
|
||||||
'download': string;
|
'download': string;
|
||||||
|
'downloadIcon': string;
|
||||||
'history': string;
|
'history': string;
|
||||||
'indexer': string;
|
'indexer': string;
|
||||||
'indexerFlags': string;
|
'indexerFlags': string;
|
||||||
'language': string;
|
'interactiveIcon': string;
|
||||||
|
'languages': string;
|
||||||
|
'manualDownloadContent': string;
|
||||||
'peers': string;
|
'peers': string;
|
||||||
'protocol': string;
|
'protocol': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import Peers from './Peers';
|
|
||||||
import styles from './InteractiveSearchRow.css';
|
|
||||||
|
|
||||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return icons.SPINNER;
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
} else if (grabError) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons.DOWNLOAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return '';
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return translate('AddedToDownloadQueue');
|
|
||||||
} else if (grabError) {
|
|
||||||
return grabError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate('AddToDownloadQueue');
|
|
||||||
}
|
|
||||||
|
|
||||||
class InteractiveSearchRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isConfirmGrabModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onGrabPress({
|
|
||||||
guid,
|
|
||||||
indexerId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmGrabPress = () => {
|
|
||||||
this.setState({ isConfirmGrabModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabConfirm = () => {
|
|
||||||
this.setState({ isConfirmGrabModalOpen: false });
|
|
||||||
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
searchPayload,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onGrabPress({
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
...searchPayload
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabCancel = () => {
|
|
||||||
this.setState({ isConfirmGrabModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
protocol,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
publishDate,
|
|
||||||
title,
|
|
||||||
infoUrl,
|
|
||||||
indexer,
|
|
||||||
size,
|
|
||||||
seeders,
|
|
||||||
leechers,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
customFormatScore,
|
|
||||||
languages,
|
|
||||||
indexerFlags,
|
|
||||||
rejections,
|
|
||||||
downloadAllowed,
|
|
||||||
isGrabbing,
|
|
||||||
isGrabbed,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
grabError,
|
|
||||||
historyGrabbedData,
|
|
||||||
historyFailedData,
|
|
||||||
blocklistData
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableRowCell className={styles.protocol}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.age}
|
|
||||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
|
|
||||||
>
|
|
||||||
{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}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.indexer}>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.history}>
|
|
||||||
{
|
|
||||||
historyGrabbedData?.date && !historyFailedData?.date &&
|
|
||||||
<Icon
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
kind={kinds.DEFAULT}
|
|
||||||
title={`${translate('Grabbed')}: ${formatDateTime(historyGrabbedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
historyFailedData?.date &&
|
|
||||||
<Icon
|
|
||||||
className={styles.failed}
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={`${translate('Failed')}: ${formatDateTime(historyFailedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklistData?.date &&
|
|
||||||
<Icon
|
|
||||||
className={historyGrabbedData || historyFailedData ? styles.blocklist : ''}
|
|
||||||
name={icons.BLOCKLIST}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={`${translate('Blocklisted')}: ${formatDateTime(blocklistData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.size}>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.peers}>
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.language}>
|
|
||||||
<MovieLanguage
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.quality}>
|
|
||||||
<MovieQuality
|
|
||||||
quality={quality}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormat}>
|
|
||||||
<MovieFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
|
||||||
{customFormatScore > 0 && `+${customFormatScore}`}
|
|
||||||
{customFormatScore < 0 && customFormatScore}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.indexerFlags}>
|
|
||||||
{
|
|
||||||
!!indexerFlags.length &&
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.FLAG}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('IndexerFlags')}
|
|
||||||
body={
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
indexerFlags.map((flag, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
{flag}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={this.state.isConfirmGrabModalOpen}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('GrabRelease')}
|
|
||||||
message={translate('GrabReleaseMessageText', [title])}
|
|
||||||
confirmLabel={translate('Grab')}
|
|
||||||
onConfirm={this.onGrabConfirm}
|
|
||||||
onCancel={this.onGrabCancel}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InteractiveSearchRow.propTypes = {
|
|
||||||
guid: PropTypes.string.isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
age: PropTypes.number.isRequired,
|
|
||||||
ageHours: PropTypes.number.isRequired,
|
|
||||||
ageMinutes: PropTypes.number.isRequired,
|
|
||||||
publishDate: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
infoUrl: PropTypes.string.isRequired,
|
|
||||||
indexerId: PropTypes.number.isRequired,
|
|
||||||
indexer: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
seeders: PropTypes.number,
|
|
||||||
leechers: PropTypes.number,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
downloadAllowed: PropTypes.bool.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isGrabbed: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.string,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
searchPayload: PropTypes.object.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
historyFailedData: PropTypes.object,
|
|
||||||
historyGrabbedData: PropTypes.object,
|
|
||||||
blocklistData: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
InteractiveSearchRow.defaultProps = {
|
|
||||||
rejections: [],
|
|
||||||
isGrabbing: false,
|
|
||||||
isGrabbed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InteractiveSearchRow;
|
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import MovieBlocklist from 'typings/MovieBlocklist';
|
||||||
|
import MovieHistory from 'typings/MovieHistory';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
|
||||||
|
import Peers from './Peers';
|
||||||
|
import styles from './InteractiveSearchRow.css';
|
||||||
|
|
||||||
|
function getDownloadIcon(
|
||||||
|
isGrabbing: boolean,
|
||||||
|
isGrabbed: boolean,
|
||||||
|
grabError?: string
|
||||||
|
) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return icons.SPINNER;
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
} else if (grabError) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons.DOWNLOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
|
||||||
|
if (isGrabbed) {
|
||||||
|
return kinds.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grabError) {
|
||||||
|
return kinds.DANGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinds.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadTooltip(
|
||||||
|
isGrabbing: boolean,
|
||||||
|
isGrabbed: boolean,
|
||||||
|
grabError?: string
|
||||||
|
) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return '';
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return translate('AddToDownloadQueue');
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('AddedToDownloadQueue');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InteractiveSearchRowProps {
|
||||||
|
guid: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
age: number;
|
||||||
|
ageHours: number;
|
||||||
|
ageMinutes: number;
|
||||||
|
publishDate: string;
|
||||||
|
title: string;
|
||||||
|
infoUrl: string;
|
||||||
|
indexerId: number;
|
||||||
|
indexer: string;
|
||||||
|
size: number;
|
||||||
|
seeders?: number;
|
||||||
|
leechers?: number;
|
||||||
|
quality: QualityModel;
|
||||||
|
languages: Language[];
|
||||||
|
customFormats: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
mappedMovieId?: number;
|
||||||
|
rejections: string[];
|
||||||
|
indexerFlags: string[];
|
||||||
|
downloadAllowed: boolean;
|
||||||
|
isGrabbing: boolean;
|
||||||
|
isGrabbed: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
historyFailedData?: MovieHistory;
|
||||||
|
historyGrabbedData?: MovieHistory;
|
||||||
|
blocklistData?: MovieBlocklist;
|
||||||
|
longDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
searchPayload: object;
|
||||||
|
onGrabPress(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
protocol,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishDate,
|
||||||
|
title,
|
||||||
|
infoUrl,
|
||||||
|
indexer,
|
||||||
|
size,
|
||||||
|
seeders,
|
||||||
|
leechers,
|
||||||
|
quality,
|
||||||
|
languages,
|
||||||
|
customFormatScore,
|
||||||
|
customFormats,
|
||||||
|
mappedMovieId,
|
||||||
|
rejections = [],
|
||||||
|
indexerFlags = [],
|
||||||
|
downloadAllowed,
|
||||||
|
isGrabbing = false,
|
||||||
|
isGrabbed = false,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
grabError,
|
||||||
|
historyGrabbedData,
|
||||||
|
historyFailedData,
|
||||||
|
blocklistData,
|
||||||
|
searchPayload,
|
||||||
|
onGrabPress,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
|
||||||
|
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const onGrabPressWrapper = useCallback(() => {
|
||||||
|
if (downloadAllowed) {
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConfirmGrabModalOpen(true);
|
||||||
|
}, [
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
downloadAllowed,
|
||||||
|
onGrabPress,
|
||||||
|
setIsConfirmGrabModalOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onGrabConfirm = useCallback(() => {
|
||||||
|
setIsConfirmGrabModalOpen(false);
|
||||||
|
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
...searchPayload,
|
||||||
|
});
|
||||||
|
}, [guid, indexerId, searchPayload, onGrabPress, setIsConfirmGrabModalOpen]);
|
||||||
|
|
||||||
|
const onGrabCancel = useCallback(() => {
|
||||||
|
setIsConfirmGrabModalOpen(false);
|
||||||
|
}, [setIsConfirmGrabModalOpen]);
|
||||||
|
|
||||||
|
const onOverridePress = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(true);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const onOverrideModalClose = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(false);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell className={styles.protocol}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.age}
|
||||||
|
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.download}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={getDownloadKind(isGrabbed, grabError)}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPressWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.manualDownloadContent}
|
||||||
|
title={translate('OverrideAndAddToDownloadQueue')}
|
||||||
|
onPress={onOverridePress}
|
||||||
|
>
|
||||||
|
<div className={styles.manualDownloadContent}>
|
||||||
|
<Icon
|
||||||
|
className={styles.interactiveIcon}
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
className={styles.downloadIcon}
|
||||||
|
name={icons.CIRCLE_DOWN}
|
||||||
|
size={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</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.RIGHT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.title}>
|
||||||
|
<Link to={infoUrl} title={title}>
|
||||||
|
<div>{title}</div>
|
||||||
|
</Link>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.history}>
|
||||||
|
{historyGrabbedData?.date && !historyFailedData?.date ? (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
title={`${translate('Grabbed')}: ${formatDateTime(
|
||||||
|
historyGrabbedData.date,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
{ includeSeconds: true }
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{historyFailedData?.date ? (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={`${translate('Failed')}: ${formatDateTime(
|
||||||
|
historyFailedData.date,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
{ includeSeconds: true }
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{blocklistData?.date ? (
|
||||||
|
<Icon
|
||||||
|
className={
|
||||||
|
historyGrabbedData || historyFailedData ? styles.blocklist : ''
|
||||||
|
}
|
||||||
|
name={icons.BLOCKLIST}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={`${translate('Blocklisted')}: ${formatDateTime(
|
||||||
|
blocklistData.date,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
{ includeSeconds: true }
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.size}>{formatBytes(size)}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.peers}>
|
||||||
|
{protocol === 'torrent' ? (
|
||||||
|
<Peers seeders={seeders} leechers={leechers} />
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.languages}>
|
||||||
|
<MovieLanguage languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.quality}>
|
||||||
|
<MovieQuality quality={quality} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.customFormat}>
|
||||||
|
<MovieFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.indexerFlags}>
|
||||||
|
{indexerFlags.length ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={
|
||||||
|
<ul>
|
||||||
|
{indexerFlags.map((flag, index) => {
|
||||||
|
return <li key={index}>{flag}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmGrabModalOpen}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={translate('GrabRelease')}
|
||||||
|
message={translate('GrabReleaseMessageText', { title })}
|
||||||
|
confirmLabel={translate('Grab')}
|
||||||
|
onConfirm={onGrabConfirm}
|
||||||
|
onCancel={onGrabCancel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OverrideMatchModal
|
||||||
|
isOpen={isOverrideModalOpen}
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
movieId={mappedMovieId}
|
||||||
|
languages={languages}
|
||||||
|
quality={quality}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onOverrideModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InteractiveSearchRow;
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import SelectDownloadClientModalContent from './SelectDownloadClientModalContent';
|
||||||
|
|
||||||
|
interface SelectDownloadClientModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
modalTitle: string;
|
||||||
|
onDownloadClientSelect(downloadClientId: number): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
|
||||||
|
const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||||
|
<SelectDownloadClientModalContent
|
||||||
|
protocol={protocol}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onDownloadClientSelect={onDownloadClientSelect}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectDownloadClientModal;
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import SelectDownloadClientRow from './SelectDownloadClientRow';
|
||||||
|
|
||||||
|
interface SelectDownloadClientModalContentProps {
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
modalTitle: string;
|
||||||
|
onDownloadClientSelect(downloadClientId: number): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectDownloadClientModalContent(
|
||||||
|
props: SelectDownloadClientModalContentProps
|
||||||
|
) {
|
||||||
|
const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props;
|
||||||
|
|
||||||
|
const { isFetching, isPopulated, error, items } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('SelectDownloadClientModalTitle', { modalTitle })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('DownloadClientsLoadError')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !error ? (
|
||||||
|
<Form>
|
||||||
|
{items.map((downloadClient) => {
|
||||||
|
const { id, name, priority } = downloadClient;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectDownloadClientRow
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
priority={priority}
|
||||||
|
onDownloadClientSelect={onDownloadClientSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectDownloadClientModalContent;
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
.downloadClient {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--borderColor);
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'downloadClient': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SelectDownloadClientRow.css';
|
||||||
|
|
||||||
|
interface SelectSeasonRowProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
priority: number;
|
||||||
|
onDownloadClientSelect(downloadClientId: number): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectDownloadClientRow(props: SelectSeasonRowProps) {
|
||||||
|
const { id, name, priority, onDownloadClientSelect } = props;
|
||||||
|
|
||||||
|
const onSeasonSelectWrapper = useCallback(() => {
|
||||||
|
onDownloadClientSelect(id);
|
||||||
|
}, [id, onDownloadClientSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={styles.downloadClient}
|
||||||
|
component="div"
|
||||||
|
onPress={onSeasonSelectWrapper}
|
||||||
|
>
|
||||||
|
<div>{name}</div>
|
||||||
|
<div>{translate('PrioritySettings', { priority })}</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectDownloadClientRow;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.link {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
display: inline-block;
|
||||||
|
margin: -2px 0;
|
||||||
|
width: 100%;
|
||||||
|
outline: 2px dashed var(--dangerColor);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optional {
|
||||||
|
outline: 2px dashed var(--gray);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'link': string;
|
||||||
|
'optional': string;
|
||||||
|
'placeholder': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import styles from './OverrideMatchData.css';
|
||||||
|
|
||||||
|
interface OverrideMatchDataProps {
|
||||||
|
value?: string | number | JSX.Element | JSX.Element[];
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isOptional?: boolean;
|
||||||
|
onPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverrideMatchData(props: OverrideMatchDataProps) {
|
||||||
|
const { value, isDisabled = false, isOptional, onPress } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link className={styles.link} isDisabled={isDisabled} onPress={onPress}>
|
||||||
|
{(value == null || (Array.isArray(value) && value.length === 0)) &&
|
||||||
|
!isDisabled ? (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
styles.placeholder,
|
||||||
|
isOptional && styles.optional
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideMatchData;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import OverrideMatchModalContent from './OverrideMatchModalContent';
|
||||||
|
|
||||||
|
interface OverrideMatchModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
indexerId: number;
|
||||||
|
guid: string;
|
||||||
|
movieId?: number;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
isGrabbing: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverrideMatchModal(props: OverrideMatchModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
movieId,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
protocol,
|
||||||
|
isGrabbing,
|
||||||
|
grabError,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||||
|
<OverrideMatchModalContent
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
movieId={movieId}
|
||||||
|
languages={languages}
|
||||||
|
quality={quality}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideMatchModal;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
.label {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-right: 20px;
|
||||||
|
color: var(--dangerColor);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.item {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'buttons': string;
|
||||||
|
'error': string;
|
||||||
|
'footer': string;
|
||||||
|
'item': string;
|
||||||
|
'label': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||||
|
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
||||||
|
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||||
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import { createMovieSelectorForHook } from 'Store/Selectors/createMovieSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
|
||||||
|
import OverrideMatchData from './OverrideMatchData';
|
||||||
|
import styles from './OverrideMatchModalContent.css';
|
||||||
|
|
||||||
|
type SelectType =
|
||||||
|
| 'select'
|
||||||
|
| 'movie'
|
||||||
|
| 'quality'
|
||||||
|
| 'language'
|
||||||
|
| 'downloadClient';
|
||||||
|
|
||||||
|
interface OverrideMatchModalContentProps {
|
||||||
|
indexerId: number;
|
||||||
|
title: string;
|
||||||
|
guid: string;
|
||||||
|
movieId?: number;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
isGrabbing: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
||||||
|
const modalTitle = translate('ManualGrab');
|
||||||
|
const {
|
||||||
|
indexerId,
|
||||||
|
title,
|
||||||
|
guid,
|
||||||
|
protocol,
|
||||||
|
isGrabbing,
|
||||||
|
grabError,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [movieId, setMovieId] = useState(props.movieId);
|
||||||
|
const [languages, setLanguages] = useState(props.languages);
|
||||||
|
const [quality, setQuality] = useState(props.quality);
|
||||||
|
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const previousIsGrabbing = usePrevious(isGrabbing);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const movie: Movie | undefined = useSelector(
|
||||||
|
createMovieSelectorForHook(movieId)
|
||||||
|
);
|
||||||
|
const { items: downloadClients } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectModalClose = useCallback(() => {
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onSelectMoviePress = useCallback(() => {
|
||||||
|
setSelectModalOpen('movie');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onMovieSelect = useCallback(
|
||||||
|
(m: Movie) => {
|
||||||
|
setMovieId(m.id);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setMovieId, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectQualityPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('quality');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onQualitySelect = useCallback(
|
||||||
|
(quality: QualityModel) => {
|
||||||
|
setQuality(quality);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setQuality, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectLanguagesPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('language');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onLanguagesSelect = useCallback(
|
||||||
|
(languages: Language[]) => {
|
||||||
|
setLanguages(languages);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setLanguages, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectDownloadClientPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('downloadClient');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onDownloadClientSelect = useCallback(
|
||||||
|
(downloadClientId: number) => {
|
||||||
|
setDownloadClientId(downloadClientId);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setDownloadClientId, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGrabPress = useCallback(() => {
|
||||||
|
if (!movieId) {
|
||||||
|
setError(translate('OverrideGrabNoMovie'));
|
||||||
|
return;
|
||||||
|
} else if (!quality) {
|
||||||
|
setError(translate('OverrideGrabNoQuality'));
|
||||||
|
return;
|
||||||
|
} else if (!languages.length) {
|
||||||
|
setError(translate('OverrideGrabNoLanguage'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
grabRelease({
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
movieId,
|
||||||
|
quality,
|
||||||
|
languages,
|
||||||
|
downloadClientId,
|
||||||
|
shouldOverride: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
movieId,
|
||||||
|
quality,
|
||||||
|
languages,
|
||||||
|
downloadClientId,
|
||||||
|
setError,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGrabbing && previousIsGrabbing) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isGrabbing, previousIsGrabbing, onModalClose]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
dispatch(fetchDownloadClients());
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('OverrideGrabModalTitle', { title })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Movie')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={movie?.title}
|
||||||
|
onPress={onSelectMoviePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Quality')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={
|
||||||
|
<MovieQuality className={styles.label} quality={quality} />
|
||||||
|
}
|
||||||
|
onPress={onSelectQualityPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Languages')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={
|
||||||
|
<MovieLanguage
|
||||||
|
className={styles.label}
|
||||||
|
languages={languages}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onPress={onSelectLanguagesPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={
|
||||||
|
downloadClients.find(
|
||||||
|
(downloadClient) => downloadClient.id === downloadClientId
|
||||||
|
)?.name ?? translate('Default')
|
||||||
|
}
|
||||||
|
onPress={onSelectDownloadClientPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.footer}>
|
||||||
|
<div className={styles.error}>{error || grabError}</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
error={grabError}
|
||||||
|
onPress={onGrabPress}
|
||||||
|
>
|
||||||
|
{translate('GrabRelease')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<SelectMovieModal
|
||||||
|
isOpen={selectModalOpen === 'movie'}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onMovieSelect={onMovieSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectQualityModal
|
||||||
|
isOpen={selectModalOpen === 'quality'}
|
||||||
|
qualityId={quality ? quality.quality.id : 0}
|
||||||
|
proper={quality ? quality.revision.version > 1 : false}
|
||||||
|
real={quality ? quality.revision.real > 0 : false}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onQualitySelect={onQualitySelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectLanguageModal
|
||||||
|
isOpen={selectModalOpen === 'language'}
|
||||||
|
languageIds={languages ? languages.map((l) => l.id) : []}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onLanguagesSelect={onLanguagesSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectDownloadClientModal
|
||||||
|
isOpen={selectModalOpen === 'downloadClient'}
|
||||||
|
protocol={protocol}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onDownloadClientSelect={onDownloadClientSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideMatchModalContent;
|
||||||
@@ -117,7 +117,7 @@ class DeleteMovieModalContent extends Component {
|
|||||||
deleteFiles &&
|
deleteFiles &&
|
||||||
<div className={styles.deleteFilesMessage}>
|
<div className={styles.deleteFilesMessage}>
|
||||||
<div>
|
<div>
|
||||||
{translate('DeleteTheMovieFolder', [path])}
|
{translate('DeleteTheMovieFolder', { path })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -654,7 +654,7 @@ class MovieDetails extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
|
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
|
||||||
<TabList
|
<TabList
|
||||||
className={styles.tabList}
|
className={styles.tabList}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
|
||||||
class MovieTitlesRow extends Component {
|
class MovieTitlesRow extends Component {
|
||||||
@@ -13,13 +12,9 @@ class MovieTitlesRow extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
language,
|
|
||||||
sourceType
|
sourceType
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// TODO - Fix languages to all take arrays
|
|
||||||
const languages = [language];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
||||||
@@ -27,12 +22,6 @@ class MovieTitlesRow extends Component {
|
|||||||
{title}
|
{title}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
|
||||||
<MovieLanguage
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
{titleCase(sourceType)}
|
{titleCase(sourceType)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
@@ -45,7 +34,6 @@ class MovieTitlesRow extends Component {
|
|||||||
MovieTitlesRow.propTypes = {
|
MovieTitlesRow.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
language: PropTypes.object.isRequired,
|
|
||||||
sourceType: PropTypes.string.isRequired
|
sourceType: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ const columns = [
|
|||||||
label: () => translate('AlternativeTitle'),
|
label: () => translate('AlternativeTitle'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'language',
|
|
||||||
label: () => translate('Language'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'sourceType',
|
name: 'sourceType',
|
||||||
label: () => translate('Type'),
|
label: () => translate('Type'),
|
||||||
|
|||||||
@@ -9,3 +9,9 @@
|
|||||||
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customFormatScore {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
|
'customFormatScore': string;
|
||||||
'sourceTitle': string;
|
'sourceTitle': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
@@ -106,8 +107,15 @@ class MovieHistoryRow extends Component {
|
|||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
{formatCustomFormatScore(customFormatScore)}
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
|
|||||||
@@ -100,6 +100,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
|||||||
{translate('DigitalRelease')}
|
{translate('DigitalRelease')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="tmdbRating"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('TmdbRating')}
|
||||||
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="imdbRating"
|
name="imdbRating"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
@@ -110,12 +119,12 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
|||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="tmdbRating"
|
name="rottenTomatoesRating"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('TmdbRating')}
|
{translate('RottenTomatoesRating')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import React, { useCallback, useState } from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
@@ -44,6 +47,9 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
showQualityProfile,
|
showQualityProfile,
|
||||||
showCinemaRelease,
|
showCinemaRelease,
|
||||||
showReleaseDate,
|
showReleaseDate,
|
||||||
|
showTmdbRating,
|
||||||
|
showImdbRating,
|
||||||
|
showRottenTomatoesRating,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = useSelector(selectPosterOptions);
|
} = useSelector(selectPosterOptions);
|
||||||
|
|
||||||
@@ -257,6 +263,24 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showTmdbRating && !!ratings.tmdb ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<TmdbRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showImdbRating && !!ratings.imdb ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<ImdbRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showRottenTomatoesRating && !!ratings.rottenTomatoes ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<RottenTomatoRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<MovieIndexPosterInfo
|
<MovieIndexPosterInfo
|
||||||
studio={studio}
|
studio={studio}
|
||||||
qualityProfile={qualityProfile}
|
qualityProfile={qualityProfile}
|
||||||
@@ -279,6 +303,9 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
certification={certification}
|
certification={certification}
|
||||||
originalTitle={originalTitle}
|
originalTitle={originalTitle}
|
||||||
originalLanguage={originalLanguage}
|
originalLanguage={originalLanguage}
|
||||||
|
showTmdbRating={showTmdbRating}
|
||||||
|
showImdbRating={showImdbRating}
|
||||||
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditMovieModalConnector
|
<EditMovieModalConnector
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { Language, Ratings } from 'Movie/Movie';
|
import { Language, Ratings } from 'Movie/Movie';
|
||||||
@@ -33,6 +34,9 @@ interface MovieIndexPosterInfoProps {
|
|||||||
shortDateFormat: string;
|
shortDateFormat: string;
|
||||||
longDateFormat: string;
|
longDateFormat: string;
|
||||||
timeFormat: string;
|
timeFormat: string;
|
||||||
|
showTmdbRating: boolean;
|
||||||
|
showImdbRating: boolean;
|
||||||
|
showRottenTomatoesRating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||||
@@ -58,6 +62,9 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
longDateFormat,
|
longDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
|
showTmdbRating,
|
||||||
|
showImdbRating,
|
||||||
|
showRottenTomatoesRating,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (sortKey === 'studio' && studio) {
|
if (sortKey === 'studio' && studio) {
|
||||||
@@ -163,7 +170,15 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortKey === 'imdbRating' && !!ratings.imdb) {
|
if (!showTmdbRating && sortKey === 'tmdbRating' && !!ratings.tmdb) {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
<TmdbRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showImdbRating && sortKey === 'imdbRating' && !!ratings.imdb) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<ImdbRating ratings={ratings} iconSize={12} />
|
<ImdbRating ratings={ratings} iconSize={12} />
|
||||||
@@ -171,10 +186,14 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortKey === 'tmdbRating' && !!ratings.tmdb) {
|
if (
|
||||||
|
!showRottenTomatoesRating &&
|
||||||
|
sortKey === 'rottenTomatoesRating' &&
|
||||||
|
!!ratings.rottenTomatoes
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<TmdbRating ratings={ratings} iconSize={12} />
|
<RottenTomatoRating ratings={ratings} iconSize={12} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,9 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
showQualityProfile,
|
showQualityProfile,
|
||||||
showCinemaRelease,
|
showCinemaRelease,
|
||||||
showReleaseDate,
|
showReleaseDate,
|
||||||
|
showTmdbRating,
|
||||||
|
showImdbRating,
|
||||||
|
showRottenTomatoesRating,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
const nextAiringHeight = 19;
|
const nextAiringHeight = 19;
|
||||||
@@ -176,12 +179,22 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTmdbRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImdbRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showRottenTomatoesRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
switch (sortKey) {
|
switch (sortKey) {
|
||||||
case 'studio':
|
case 'studio':
|
||||||
case 'added':
|
case 'added':
|
||||||
case 'year':
|
case 'year':
|
||||||
case 'imdbRating':
|
|
||||||
case 'tmdbRating':
|
|
||||||
case 'path':
|
case 'path':
|
||||||
case 'sizeOnDisk':
|
case 'sizeOnDisk':
|
||||||
case 'originalTitle':
|
case 'originalTitle':
|
||||||
@@ -204,6 +217,21 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'imdbRating':
|
||||||
|
if (!showImdbRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'tmdbRating':
|
||||||
|
if (!showTmdbRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'rottenTomatoesRating':
|
||||||
|
if (!showRottenTomatoesRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// No need to add a height of 0
|
// No need to add a height of 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
showQualityProfile,
|
showQualityProfile,
|
||||||
showCinemaRelease,
|
showCinemaRelease,
|
||||||
showReleaseDate,
|
showReleaseDate,
|
||||||
|
showTmdbRating,
|
||||||
|
showImdbRating,
|
||||||
|
showRottenTomatoesRating,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
@@ -156,6 +159,42 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowTmdbRating')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTmdbRating"
|
||||||
|
value={showTmdbRating}
|
||||||
|
helpText={translate('ShowTmdbRatingHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowImdbRating')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showImdbRating"
|
||||||
|
value={showImdbRating}
|
||||||
|
helpText={translate('ShowImdbRatingHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowRottenTomatoesRating')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showRottenTomatoesRating"
|
||||||
|
value={showRottenTomatoesRating}
|
||||||
|
helpText={translate('ShowRottenTomatoesRatingHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ function EditMoviesModalContent(props: EditMoviesModalContentProps) {
|
|||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('MoviesSelectedInterp', [selectedCount])}
|
{translate('MoviesSelectedInterp', { count: selectedCount })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ function MovieIndexSelectFooter() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('MoviesSelectedInterp', [selectedCount])}
|
{translate('MoviesSelectedInterp', { count: selectedCount })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditMoviesModal
|
<EditMoviesModal
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function OrganizeMoviesModalContent(props: OrganizeMoviesModalContentProps) {
|
|||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
{translate('OrganizeConfirm', [movieTitles.length])}
|
{translate('OrganizeConfirm', { count: movieTitles.length })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class FileEditModalContent extends Component {
|
|||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToLoadQualities')}
|
{translate('QualitiesLoadError')}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,31 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.languages,
|
||||||
|
.audio,
|
||||||
|
.video,
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audioLanguages,
|
||||||
|
.videoDynamicRangeType,
|
||||||
|
.subtitles {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 165px;
|
||||||
|
}
|
||||||
|
|
||||||
.releaseGroup {
|
.releaseGroup {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customFormatScore {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,14 +3,21 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'age': string;
|
'age': string;
|
||||||
|
'audio': string;
|
||||||
|
'audioLanguages': string;
|
||||||
|
'customFormatScore': string;
|
||||||
'download': string;
|
'download': string;
|
||||||
'formats': string;
|
'formats': string;
|
||||||
'language': string;
|
'language': string;
|
||||||
|
'languages': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
'rejected': string;
|
'rejected': string;
|
||||||
'relativePath': string;
|
'relativePath': string;
|
||||||
'releaseGroup': string;
|
'releaseGroup': string;
|
||||||
'size': string;
|
'size': string;
|
||||||
|
'subtitles': string;
|
||||||
|
'video': string;
|
||||||
|
'videoDynamicRangeType': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import IconButton from 'Components/Link/IconButton';
|
|||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
@@ -12,6 +13,7 @@ import FileEditModal from 'MovieFile/Edit/FileEditModal';
|
|||||||
import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
|
import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
|
||||||
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FileDetailsModal from '../FileDetailsModal';
|
import FileDetailsModal from '../FileDetailsModal';
|
||||||
import MovieFileRowCellPlaceholder from './MovieFileRowCellPlaceholder';
|
import MovieFileRowCellPlaceholder from './MovieFileRowCellPlaceholder';
|
||||||
@@ -78,7 +80,9 @@ class MovieFileEditorRow extends Component {
|
|||||||
quality,
|
quality,
|
||||||
qualityCutoffNotMet,
|
qualityCutoffNotMet,
|
||||||
customFormats,
|
customFormats,
|
||||||
languages
|
customFormatScore,
|
||||||
|
languages,
|
||||||
|
columns
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -88,107 +92,228 @@ class MovieFileEditorRow extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const showQualityPlaceholder = !quality;
|
const showQualityPlaceholder = !quality;
|
||||||
|
|
||||||
const showLanguagePlaceholder = !languages;
|
const showLanguagePlaceholder = !languages;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableRowCell
|
{
|
||||||
className={styles.relativePath}
|
columns.map((column) => {
|
||||||
title={relativePath}
|
const {
|
||||||
>
|
name,
|
||||||
{relativePath}
|
isVisible
|
||||||
</TableRowCell>
|
} = column;
|
||||||
|
|
||||||
<TableRowCell>
|
if (!isVisible) {
|
||||||
<MediaInfoConnector
|
return null;
|
||||||
movieFileId={id}
|
}
|
||||||
type={mediaInfoTypes.VIDEO}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>
|
if (name === 'relativePath') {
|
||||||
<MediaInfoConnector
|
return (
|
||||||
movieFileId={id}
|
<TableRowCell
|
||||||
type={mediaInfoTypes.AUDIO}
|
key={name}
|
||||||
/>
|
className={styles.relativePath}
|
||||||
</TableRowCell>
|
title={relativePath}
|
||||||
|
>
|
||||||
|
{relativePath}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<TableRowCell
|
if (name === 'customFormats') {
|
||||||
className={styles.size}
|
return (
|
||||||
title={size}
|
<TableRowCell key={name}>
|
||||||
>
|
<MovieFormats
|
||||||
{formatBytes(size)}
|
formats={customFormats}
|
||||||
</TableRowCell>
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<TableRowCell
|
if (name === 'customFormatScore') {
|
||||||
className={styles.language}
|
return (
|
||||||
>
|
<TableRowCell
|
||||||
{
|
key={name}
|
||||||
showLanguagePlaceholder &&
|
className={styles.customFormatScore}
|
||||||
<MovieFileRowCellPlaceholder />
|
>
|
||||||
}
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
if (name === 'languages') {
|
||||||
!showLanguagePlaceholder && !!languages &&
|
return (
|
||||||
<MovieLanguage
|
<TableRowCell
|
||||||
className={styles.label}
|
key={name}
|
||||||
languages={languages}
|
className={styles.languages}
|
||||||
/>
|
>
|
||||||
}
|
{
|
||||||
</TableRowCell>
|
showLanguagePlaceholder ?
|
||||||
|
<MovieFileRowCellPlaceholder /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<TableRowCell
|
{
|
||||||
className={styles.quality}
|
!showLanguagePlaceholder && !!languages &&
|
||||||
>
|
<MovieLanguage
|
||||||
{
|
className={styles.label}
|
||||||
showQualityPlaceholder &&
|
languages={languages}
|
||||||
<MovieFileRowCellPlaceholder />
|
/>
|
||||||
}
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
if (name === 'quality') {
|
||||||
!showQualityPlaceholder && !!quality &&
|
return (
|
||||||
<MovieQuality
|
<TableRowCell
|
||||||
className={styles.label}
|
key={name}
|
||||||
quality={quality}
|
className={styles.quality}
|
||||||
isCutoffNotMet={qualityCutoffNotMet}
|
>
|
||||||
/>
|
{
|
||||||
}
|
showQualityPlaceholder ?
|
||||||
</TableRowCell>
|
<MovieFileRowCellPlaceholder /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<TableRowCell
|
{
|
||||||
className={styles.releaseGroup}
|
!showQualityPlaceholder && !!quality &&
|
||||||
>
|
<MovieQuality
|
||||||
{releaseGroup}
|
className={styles.label}
|
||||||
</TableRowCell>
|
quality={quality}
|
||||||
|
isCutoffNotMet={qualityCutoffNotMet}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<TableRowCell
|
if (name === 'audioInfo') {
|
||||||
className={styles.formats}
|
return (
|
||||||
>
|
<TableRowCell
|
||||||
<MovieFormats
|
key={name}
|
||||||
formats={customFormats}
|
className={styles.audio}
|
||||||
/>
|
>
|
||||||
</TableRowCell>
|
<MediaInfoConnector
|
||||||
|
type={mediaInfoTypes.AUDIO}
|
||||||
|
movieFileId={id}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
if (name === 'audioLanguages') {
|
||||||
<IconButton
|
return (
|
||||||
title={translate('EditMovieFile')}
|
<TableRowCell
|
||||||
name={icons.EDIT}
|
key={name}
|
||||||
onPress={this.onFileEditPress}
|
className={styles.audioLanguages}
|
||||||
/>
|
>
|
||||||
|
<MediaInfoConnector
|
||||||
|
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
||||||
|
movieFileId={id}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<IconButton
|
if (name === 'subtitleLanguages') {
|
||||||
title={translate('Details')}
|
return (
|
||||||
name={icons.MEDIA_INFO}
|
<TableRowCell
|
||||||
onPress={this.onFileDetailsPress}
|
key={name}
|
||||||
/>
|
className={styles.subtitles}
|
||||||
|
>
|
||||||
|
<MediaInfoConnector
|
||||||
|
type={mediaInfoTypes.SUBTITLES}
|
||||||
|
movieFileId={id}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<IconButton
|
if (name === 'videoCodec') {
|
||||||
title={translate('DeleteFile')}
|
return (
|
||||||
name={icons.REMOVE}
|
<TableRowCell
|
||||||
onPress={this.onDeletePress}
|
key={name}
|
||||||
/>
|
className={styles.video}
|
||||||
</TableRowCell>
|
>
|
||||||
|
<MediaInfoConnector
|
||||||
|
type={mediaInfoTypes.VIDEO}
|
||||||
|
movieFileId={id}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'videoDynamicRangeType') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.videoDynamicRangeType}
|
||||||
|
>
|
||||||
|
<MediaInfoConnector
|
||||||
|
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||||
|
movieFileId={id}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.size}
|
||||||
|
title={size}
|
||||||
|
>
|
||||||
|
{formatBytes(size)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'releaseGroup') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.releaseGroup}
|
||||||
|
>
|
||||||
|
{releaseGroup}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
title={translate('EditMovieFile')}
|
||||||
|
name={icons.EDIT}
|
||||||
|
onPress={this.onFileEditPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={translate('Details')}
|
||||||
|
name={icons.MEDIA_INFO}
|
||||||
|
onPress={this.onFileDetailsPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={translate('DeleteFile')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={this.onDeletePress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
<FileDetailsModal
|
<FileDetailsModal
|
||||||
isOpen={isFileDetailsModalOpen}
|
isOpen={isFileDetailsModalOpen}
|
||||||
@@ -207,7 +332,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
ids={[id]}
|
ids={[id]}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteSelectedMovieFiles')}
|
title={translate('DeleteSelectedMovieFiles')}
|
||||||
message={translate('DeleteSelectedMovieFilesMessage')}
|
message={translate('DeleteSelectedMovieFilesHelpText')}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={this.onConfirmDelete}
|
onConfirm={this.onConfirmDelete}
|
||||||
onCancel={this.onConfirmDeleteModalClose}
|
onCancel={this.onConfirmDeleteModalClose}
|
||||||
@@ -225,10 +350,16 @@ MovieFileEditorRow.propTypes = {
|
|||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
releaseGroup: PropTypes.string,
|
releaseGroup: PropTypes.string,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
mediaInfo: PropTypes.object,
|
mediaInfo: PropTypes.object,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onDeletePress: PropTypes.func.isRequired
|
onDeletePress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MovieFileEditorRow.defaultProps = {
|
||||||
|
customFormats: []
|
||||||
|
};
|
||||||
|
|
||||||
export default MovieFileEditorRow;
|
export default MovieFileEditorRow;
|
||||||
|
|||||||
@@ -1,61 +1,10 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import MovieFileEditorRow from './MovieFileEditorRow';
|
import MovieFileEditorRow from './MovieFileEditorRow';
|
||||||
import styles from './MovieFileEditorTableContent.css';
|
import styles from './MovieFileEditorTableContent.css';
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
label: () => translate('RelativePath'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'videoCodec',
|
|
||||||
label: () => translate('VideoCodec'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'audioInfo',
|
|
||||||
label: () => translate('AudioInfo'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'size',
|
|
||||||
label: () => translate('Size'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'languages',
|
|
||||||
label: () => translate('Languages'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'quality',
|
|
||||||
label: () => translate('Quality'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'releaseGroup',
|
|
||||||
label: () => translate('ReleaseGroup'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'quality.customFormats',
|
|
||||||
label: () => translate('Formats'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'action',
|
|
||||||
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class MovieFileEditorTableContent extends Component {
|
class MovieFileEditorTableContent extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -63,7 +12,9 @@ class MovieFileEditorTableContent extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items
|
items,
|
||||||
|
columns,
|
||||||
|
onTableOptionChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -77,13 +28,17 @@ class MovieFileEditorTableContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!!items.length &&
|
!!items.length &&
|
||||||
<Table columns={columns}>
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{
|
{
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<MovieFileEditorRow
|
<MovieFileEditorRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
columns={columns}
|
||||||
{...item}
|
{...item}
|
||||||
onDeletePress={this.props.onDeletePress}
|
onDeletePress={this.props.onDeletePress}
|
||||||
/>
|
/>
|
||||||
@@ -103,6 +58,8 @@ MovieFileEditorTableContent.propTypes = {
|
|||||||
movieId: PropTypes.number,
|
movieId: PropTypes.number,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
onDeletePress: PropTypes.func.isRequired
|
onDeletePress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/* eslint max-params: 0 */
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { deleteMovieFile, updateMovieFiles } from 'Store/Actions/movieFileActions';
|
import { deleteMovieFile, setMovieFilesTableOption, updateMovieFiles } from 'Store/Actions/movieFileActions';
|
||||||
import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
import getQualities from 'Utilities/Quality/getQualities';
|
import getQualities from 'Utilities/Quality/getQualities';
|
||||||
@@ -30,6 +29,7 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
items: filesForMovie,
|
items: filesForMovie,
|
||||||
|
columns: movieFiles.columns,
|
||||||
isDeleting: movieFiles.isDeleting,
|
isDeleting: movieFiles.isDeleting,
|
||||||
isSaving: movieFiles.isSaving,
|
isSaving: movieFiles.isSaving,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -54,6 +54,10 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatch(updateMovieFiles(updateProps));
|
dispatch(updateMovieFiles(updateProps));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onTableOptionChange(payload) {
|
||||||
|
dispatch(setMovieFilesTableOption(payload));
|
||||||
|
},
|
||||||
|
|
||||||
onDeletePress(movieFileId) {
|
onDeletePress(movieFileId) {
|
||||||
dispatch(deleteMovieFile({
|
dispatch(deleteMovieFile({
|
||||||
id: movieFileId
|
id: movieFileId
|
||||||
|
|||||||
@@ -1,36 +1,77 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import * as mediaInfoTypes from './mediaInfoTypes';
|
import * as mediaInfoTypes from './mediaInfoTypes';
|
||||||
|
|
||||||
|
function formatLanguages(languages) {
|
||||||
|
if (!languages) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitLanguages = _.uniq(languages.split('/')).map((l) => {
|
||||||
|
const simpleLanguage = l.split('_')[0];
|
||||||
|
|
||||||
|
if (simpleLanguage === 'und') {
|
||||||
|
return translate('Unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLanguageName(simpleLanguage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (splitLanguages.length > 3) {
|
||||||
|
return (
|
||||||
|
<span title={splitLanguages.join(', ')}>
|
||||||
|
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{splitLanguages.join(', ')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MediaInfo(props) {
|
function MediaInfo(props) {
|
||||||
const {
|
const {
|
||||||
type,
|
type,
|
||||||
audioChannels,
|
audioChannels,
|
||||||
audioCodec,
|
audioCodec,
|
||||||
videoCodec
|
audioLanguages,
|
||||||
|
subtitles,
|
||||||
|
videoCodec,
|
||||||
|
videoDynamicRangeType
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (type === mediaInfoTypes.AUDIO) {
|
if (type === mediaInfoTypes.AUDIO) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
!!audioCodec &&
|
audioCodec ? audioCodec : ''
|
||||||
audioCodec
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!audioCodec && !!audioChannels &&
|
audioCodec && audioChannels ? ' - ' : ''
|
||||||
' - '
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!audioChannels &&
|
audioChannels ? audioChannels.toFixed(1) : ''
|
||||||
audioChannels.toFixed(1)
|
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
|
||||||
|
return formatLanguages(audioLanguages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === mediaInfoTypes.SUBTITLES) {
|
||||||
|
return formatLanguages(subtitles);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === mediaInfoTypes.VIDEO) {
|
if (type === mediaInfoTypes.VIDEO) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
@@ -39,6 +80,14 @@ function MediaInfo(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{videoDynamicRangeType}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +95,10 @@ MediaInfo.propTypes = {
|
|||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
audioChannels: PropTypes.number,
|
audioChannels: PropTypes.number,
|
||||||
audioCodec: PropTypes.string,
|
audioCodec: PropTypes.string,
|
||||||
videoCodec: PropTypes.string
|
audioLanguages: PropTypes.string,
|
||||||
|
subtitles: PropTypes.string,
|
||||||
|
videoCodec: PropTypes.string,
|
||||||
|
videoDynamicRangeType: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MediaInfo;
|
export default MediaInfo;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class SelectQualityModalContent extends Component {
|
|||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToLoadQualities')}
|
{translate('QualitiesLoadError')}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
export const AUDIO = 'audio';
|
export const AUDIO = 'audio';
|
||||||
|
export const AUDIO_LANGUAGES = 'audioLanguages';
|
||||||
|
export const SUBTITLES = 'subtitles';
|
||||||
export const VIDEO = 'video';
|
export const VIDEO = 'video';
|
||||||
|
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
|
|||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { clear, fetch } from 'Store/Actions/parseActions';
|
import { clear, fetch } from 'Store/Actions/parseActions';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import ParseResult from './ParseResult';
|
import ParseResult from './ParseResult';
|
||||||
import parseStateSelector from './parseStateSelector';
|
import parseStateSelector from './parseStateSelector';
|
||||||
import styles from './Parse.css';
|
import styles from './Parse.css';
|
||||||
@@ -50,7 +51,7 @@ function Parse() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title="Parse">
|
<PageContent title={translate('Parse')}>
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<div className={styles.inputIconContainer}>
|
<div className={styles.inputIconContainer}>
|
||||||
@@ -76,7 +77,7 @@ function Parse() {
|
|||||||
{!isFetching && !!error ? (
|
{!isFetching && !!error ? (
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
Error parsing, please try again.
|
{translate('ParseModalErrorParsing')}
|
||||||
</div>
|
</div>
|
||||||
<div>{getErrorMessage(error)}</div>
|
<div>{getErrorMessage(error)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +85,7 @@ function Parse() {
|
|||||||
|
|
||||||
{!isFetching && title && !error && !item.parsedMovieInfo ? (
|
{!isFetching && title && !error && !item.parsedMovieInfo ? (
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
Unable to parse the provided title, please try again.
|
{translate('ParseModalUnableToParse')}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -95,12 +96,9 @@ function Parse() {
|
|||||||
{title ? null : (
|
{title ? null : (
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
Enter a release title in the input above
|
{translate('ParseModalHelpText')}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Radarr will attempt to parse the title and show you details about
|
|
||||||
it
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>{translate('ParseModalHelpTextDetails')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function ParseModalContent(props: ParseModalContentProps) {
|
|||||||
{!isFetching && !!error ? (
|
{!isFetching && !!error ? (
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
Error parsing, please try again.
|
{translate('ParseModalErrorParsing')}
|
||||||
</div>
|
</div>
|
||||||
<div>{getErrorMessage(error)}</div>
|
<div>{getErrorMessage(error)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@ function ParseModalContent(props: ParseModalContentProps) {
|
|||||||
|
|
||||||
{!isFetching && title && !error && !item.parsedMovieInfo ? (
|
{!isFetching && title && !error && !item.parsedMovieInfo ? (
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
Unable to parse the provided title, please try again.
|
{translate('ParseModalUnableToParse')}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -105,12 +105,9 @@ function ParseModalContent(props: ParseModalContentProps) {
|
|||||||
{title ? null : (
|
{title ? null : (
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
Enter a release title in the input above
|
{translate('ParseModalHelpText')}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Radarr will attempt to parse the title and show you details about
|
|
||||||
it
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>{translate('ParseModalHelpTextDetails')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ function ParseResult(props: ParseResultProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{tmdbId ? (
|
{tmdbId ? (
|
||||||
<ParseResultItem title={translate('TmdbId')} data={tmdbId} />
|
<ParseResultItem title={translate('TMDBId')} data={tmdbId} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{imdbId ? (
|
{imdbId ? (
|
||||||
<ParseResultItem title={translate('ImdbId')} data={imdbId} />
|
<ParseResultItem title={translate('IMDbId')} data={imdbId} />
|
||||||
) : null}
|
) : null}
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ function ParseResult(props: ParseResultProps) {
|
|||||||
|
|
||||||
<ParseResultItem
|
<ParseResultItem
|
||||||
title={translate('Repack')}
|
title={translate('Repack')}
|
||||||
data={quality.revision.isRepack ? 'True' : '-'}
|
data={quality.revision.isRepack ? translate('True') : '-'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ function ParseResult(props: ParseResultProps) {
|
|||||||
|
|
||||||
<ParseResultItem
|
<ParseResultItem
|
||||||
title={translate('Real')}
|
title={translate('Real')}
|
||||||
data={quality.revision.real ? 'True' : '-'}
|
data={quality.revision.real ? translate('True') : '-'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,7 +150,13 @@ function ParseResult(props: ParseResultProps) {
|
|||||||
|
|
||||||
<ParseResultItem
|
<ParseResultItem
|
||||||
title={translate('CustomFormats')}
|
title={translate('CustomFormats')}
|
||||||
data={<MovieFormats formats={customFormats} />}
|
data={
|
||||||
|
customFormats?.length ? (
|
||||||
|
<MovieFormats formats={customFormats} />
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ParseResultItem
|
<ParseResultItem
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export enum QualitySource {
|
export enum QualitySource {
|
||||||
Unknown = 'unkonwn',
|
Unknown = 'unknown',
|
||||||
Television = 'television',
|
Television = 'television',
|
||||||
TelevisionRaw = 'televisionRaw',
|
TelevisionRaw = 'televisionRaw',
|
||||||
Web = 'web',
|
Web = 'web',
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function RootFolderRow(props: RootFolderRowProps) {
|
|||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteRootFolder')}
|
title={translate('DeleteRootFolder')}
|
||||||
message={translate('DeleteRootFolderMessageText', [path])}
|
message={translate('DeleteRootFolderMessageText', { path })}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={onConfirmDelete}
|
onConfirm={onConfirmDelete}
|
||||||
onCancel={onDeleteModalClose}
|
onCancel={onDeleteModalClose}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
|
|||||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteCustomFormat')}
|
title={translate('DeleteCustomFormat')}
|
||||||
message={translate('DeleteCustomFormatMessageText', [name])}
|
message={translate('DeleteCustomFormatMessageText', { name })}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
isSpinning={isDeleting}
|
isSpinning={isDeleting}
|
||||||
onConfirm={this.onConfirmDeleteCustomFormat}
|
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ function EditSpecificationModalContent(props) {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onCancelPress}>
|
<ModalContent onModalClose={onCancelPress}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{`${id ? 'Edit' : 'Add'} Condition - ${implementationName}`}
|
{id ? translate('EditConditionImplementation', { implementationName }) : translate('AddConditionImplementation', { implementationName })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class Specification extends Component {
|
|||||||
isOpen={this.state.isDeleteSpecificationModalOpen}
|
isOpen={this.state.isDeleteSpecificationModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteCondition')}
|
title={translate('DeleteCondition')}
|
||||||
message={translate('DeleteConditionMessageText', [name])}
|
message={translate('DeleteConditionMessageText', { name })}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={this.onConfirmDeleteSpecification}
|
onConfirm={this.onConfirmDeleteSpecification}
|
||||||
onCancel={this.onDeleteSpecificationModalClose}
|
onCancel={this.onDeleteSpecificationModalClose}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class DownloadClient extends Component {
|
|||||||
kind={kinds.DISABLED}
|
kind={kinds.DISABLED}
|
||||||
outline={true}
|
outline={true}
|
||||||
>
|
>
|
||||||
{translate('PrioritySettings', [priority])}
|
{translate('PrioritySettings', { priority })}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +113,7 @@ class DownloadClient extends Component {
|
|||||||
isOpen={this.state.isDeleteDownloadClientModalOpen}
|
isOpen={this.state.isDeleteDownloadClientModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteDownloadClient')}
|
title={translate('DeleteDownloadClient')}
|
||||||
message={translate('DeleteDownloadClientMessageText', [name])}
|
message={translate('DeleteDownloadClientMessageText', { name })}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={this.onConfirmDeleteDownloadClient}
|
onConfirm={this.onConfirmDeleteDownloadClient}
|
||||||
onCancel={this.onDeleteDownloadClientModalClose}
|
onCancel={this.onDeleteDownloadClientModalClose}
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{`${id ? translate('Edit') : translate('Add')} ${translate('DownloadClient')} - ${implementationName}`}
|
{id ? translate('EditDownloadClientImplementation', { implementationName }) : translate('AddDownloadClientImplementation', { implementationName })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
+1
-1
@@ -180,7 +180,7 @@ function ManageDownloadClientsEditModalContent(
|
|||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('CountDownloadClientsSelected', [selectedCount])}
|
{translate('CountDownloadClientsSelected', { count: selectedCount })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+3
-3
@@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
|
|||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteSelectedDownloadClients')}
|
title={translate('DeleteSelectedDownloadClients')}
|
||||||
message={translate('DeleteSelectedDownloadClientsMessageText', [
|
message={translate('DeleteSelectedDownloadClientsMessageText', {
|
||||||
selectedIds.length,
|
count: selectedIds.length,
|
||||||
])}
|
})}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={onConfirmDelete}
|
onConfirm={onConfirmDelete}
|
||||||
onCancel={onDeleteModalClose}
|
onCancel={onDeleteModalClose}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import FieldSet from 'Components/FieldSet';
|
import FieldSet from 'Components/FieldSet';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
|
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
|
||||||
import RemotePathMapping from './RemotePathMapping';
|
import RemotePathMapping from './RemotePathMapping';
|
||||||
@@ -50,6 +52,11 @@ class RemotePathMappings extends Component {
|
|||||||
errorMessage={translate('UnableToLoadRemotePathMappings')}
|
errorMessage={translate('UnableToLoadRemotePathMappings')}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
<InlineMarkdown data={translate('RemotePathMappingsInfo', { app: 'Radarr', wikiLink: 'https://wiki.servarr.com/radarr/settings#remote-path-mappings' })} />
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<div className={styles.remotePathMappingsHeader}>
|
<div className={styles.remotePathMappingsHeader}>
|
||||||
<div className={styles.host}>
|
<div className={styles.host}>
|
||||||
{translate('Host')}
|
{translate('Host')}
|
||||||
|
|||||||
@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const authenticationMethodOptions = [
|
export const authenticationMethodOptions = [
|
||||||
{
|
{
|
||||||
key: 'none',
|
key: 'none',
|
||||||
get value() {
|
get value() {
|
||||||
return translate('None');
|
return translate('None');
|
||||||
}
|
},
|
||||||
|
isDisabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'external',
|
||||||
|
get value() {
|
||||||
|
return translate('External');
|
||||||
|
},
|
||||||
|
isHidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'basic',
|
key: 'basic',
|
||||||
@@ -32,6 +40,21 @@ const authenticationMethodOptions = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const authenticationRequiredOptions = [
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Enabled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disabledForLocalAddresses',
|
||||||
|
get value() {
|
||||||
|
return translate('DisabledForLocalAddresses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const certificateValidationOptions = [
|
const certificateValidationOptions = [
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
@@ -42,7 +65,7 @@ const certificateValidationOptions = [
|
|||||||
{
|
{
|
||||||
key: 'disabledForLocalAddresses',
|
key: 'disabledForLocalAddresses',
|
||||||
get value() {
|
get value() {
|
||||||
return translate('CertValidationNoLocal');
|
return translate('DisabledForLocalAddresses');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -98,6 +121,7 @@ class SecuritySettings extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -116,13 +140,31 @@ class SecuritySettings extends Component {
|
|||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText')}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
helpTextWarning={translate('AuthenticationRequiredWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...authenticationMethod}
|
{...authenticationMethod}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="authenticationRequired"
|
||||||
|
values={authenticationRequiredOptions}
|
||||||
|
helpText={translate('AuthenticationRequiredHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...authenticationRequired}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Username')}</FormLabel>
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
@@ -132,11 +174,12 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...username}
|
{...username}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Password')}</FormLabel>
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
@@ -146,7 +189,8 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...password}
|
{...password}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import { inputTypes, sizes } from 'Helpers/Props';
|
|||||||
import titleCase from 'Utilities/String/titleCase';
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
const branchValues = [
|
||||||
|
'master',
|
||||||
|
'develop',
|
||||||
|
'nightly'
|
||||||
|
];
|
||||||
|
|
||||||
function UpdateSettings(props) {
|
function UpdateSettings(props) {
|
||||||
const {
|
const {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
@@ -52,11 +58,12 @@ function UpdateSettings(props) {
|
|||||||
<FormLabel>{translate('Branch')}</FormLabel>
|
<FormLabel>{translate('Branch')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.AUTO_COMPLETE}
|
||||||
name="branch"
|
name="branch"
|
||||||
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
||||||
helpLink="https://wiki.servarr.com/radarr/settings#updates"
|
helpLink="https://wiki.servarr.com/radarr/settings#updates"
|
||||||
{...branch}
|
{...branch}
|
||||||
|
values={branchValues}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
readOnly={usingExternalUpdateMechanism}
|
readOnly={usingExternalUpdateMechanism}
|
||||||
/>
|
/>
|
||||||
@@ -76,6 +83,7 @@ function UpdateSettings(props) {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="updateAutomatically"
|
name="updateAutomatically"
|
||||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||||
|
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Radarr' }) : undefined}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...updateAutomatically}
|
{...updateAutomatically}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user