mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-16 21:15:33 -04:00
Compare commits
182 Commits
v4.7.2.768
...
v5.0.0.795
| 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 | ||
|
|
a6f61b2722 | ||
|
|
54bb267e17 | ||
|
|
00e2933052 | ||
|
|
b8f06eb97d | ||
|
|
bd49a4ee8b | ||
|
|
247ca9b22a | ||
|
|
779b65fa2e | ||
|
|
002cbdb864 | ||
|
|
e36715d359 | ||
|
|
69b621b13a | ||
|
|
385c7971bb | ||
|
|
1129d3901c | ||
|
|
d057d15ac7 | ||
|
|
722c20a5dc | ||
|
|
43a0e75acf | ||
|
|
abad6a9f18 | ||
|
|
835a539275 | ||
|
|
cd2d81a5aa | ||
|
|
5aee804bc0 | ||
|
|
12fcd3f9b9 | ||
|
|
47360d4d38 | ||
|
|
788782d009 | ||
|
|
847d6244aa | ||
|
|
8fd8128641 | ||
|
|
136075d233 | ||
|
|
02cec5312c | ||
|
|
e5f728352c | ||
|
|
2cc1333e5c | ||
|
|
a79980aae5 | ||
|
|
f2bbef75dd | ||
|
|
d5c1f58839 | ||
|
|
430ea81937 | ||
|
|
80099dcacb | ||
|
|
938b69b240 | ||
|
|
9839b482b2 | ||
|
|
4dbd962fca | ||
|
|
856c4fa4bb | ||
|
|
45f5ce5f29 | ||
|
|
9d3e7f62ca | ||
|
|
594ed666e1 | ||
|
|
36338310df | ||
|
|
ffde07e4d6 | ||
|
|
90a1e1dbb3 | ||
|
|
8b5f305462 | ||
|
|
2fe6847eb3 | ||
|
|
bf0f681d46 | ||
|
|
f9cb4c1abd | ||
|
|
1190bf791c | ||
|
|
53eb88d9a9 | ||
|
|
ed5c063127 | ||
|
|
e691253419 | ||
|
|
2959f72a10 | ||
|
|
78ae059f3d | ||
|
|
7226cab3d8 | ||
|
|
622162c5f6 | ||
|
|
e612d8c485 | ||
|
|
b20e15855c |
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
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']
|
||||
body:
|
||||
- type: checkboxes
|
||||
@@ -65,18 +65,18 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Trace Logs?
|
||||
label: Trace Logs? **Not Optional**
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
***Generally speaking, all bug reports MUST have trace logs provided.***
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
|
||||
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
||||
label: Trace Logs have been provided as applicable. Reports will be closed if the required logs are not provided.
|
||||
description: Trace logs are **generally required** and are not optional for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
||||
options:
|
||||
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
||||
required: true
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,6 +3,3 @@ contact_links:
|
||||
- name: Support via Discord
|
||||
url: https://radarr.video/discord
|
||||
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.
|
||||
|
||||
4
.github/workflows/support.yml
vendored
4
.github/workflows/support.yml
vendored
@@ -19,9 +19,9 @@ jobs:
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
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)
|
||||
or [Subreddit](https://reddit.com/r/radarr)
|
||||
to be a support request. Please hop over onto our [Discord](https://radarr.video/discord).
|
||||
close-issue: true
|
||||
close-reason: 'not planned'
|
||||
lock-issue: false
|
||||
- uses: dessant/support-requests@v3
|
||||
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://radarr.video/discord)
|
||||
[](https://www.reddit.com/r/Radarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.7.2'
|
||||
majorVersion: '5.0.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
@@ -27,6 +27,10 @@ trigger:
|
||||
include:
|
||||
- develop
|
||||
- master
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/Radarr.Api.*/openapi.json
|
||||
|
||||
pr:
|
||||
branches:
|
||||
@@ -34,6 +38,7 @@ pr:
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Radarr.Api.*/openapi.json
|
||||
|
||||
@@ -536,8 +541,8 @@ stages:
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
- job: Unit_LinuxCore_Postgres14
|
||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
@@ -589,7 +594,63 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
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
|
||||
|
||||
- stage: Integration
|
||||
@@ -675,8 +736,8 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
- job: Integration_LinuxCore_Postgres14
|
||||
displayName: Integration Native LinuxCore with Postgres14 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
@@ -733,7 +794,70 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
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
|
||||
displayName: Publish Test Results
|
||||
|
||||
|
||||
13
build.sh
13
build.sh
@@ -392,22 +392,21 @@ then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
|
||||
then
|
||||
YarnInstall
|
||||
RunWebpack
|
||||
fi
|
||||
|
||||
if [ "$LINT" = "YES" ];
|
||||
then
|
||||
if [ -z "$FRONTEND" ];
|
||||
then
|
||||
YarnInstall
|
||||
fi
|
||||
|
||||
LintUI
|
||||
fi
|
||||
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
then
|
||||
RunWebpack
|
||||
fi
|
||||
|
||||
if [ "$PACKAGES" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = (env) => {
|
||||
},
|
||||
|
||||
entry: {
|
||||
index: 'index.js'
|
||||
index: 'index.ts'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
@@ -97,7 +97,8 @@ module.exports = (env) => {
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.ejs',
|
||||
filename: 'index.html',
|
||||
publicPath: '/'
|
||||
publicPath: '/',
|
||||
inject: false
|
||||
}),
|
||||
|
||||
new FileManagerPlugin({
|
||||
|
||||
@@ -35,7 +35,7 @@ function QueueDetails(props) {
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ImportFailedInterp', [errorMessage])}
|
||||
title={translate('ImportFailedInterp', { errorMessage })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ function QueueDetails(props) {
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DownloadFailedInterp', [errorMessage])}
|
||||
title={translate('DownloadFailedInterp', { errorMessage })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,14 +107,14 @@ function QueueStatusCell(props) {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', [warningMessage]);
|
||||
title = translate('DownloadWarning', { warningMessage });
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('ImportFailed', [sourceTitle]);
|
||||
title = translate('ImportFailed', { sourceTitle });
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
|
||||
@@ -88,12 +88,12 @@ class RemoveQueueItemModal extends Component {
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('Remove')} - {sourceTitle}
|
||||
{translate('RemoveQueueItem', { sourceTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('RemoveFromQueueText', [sourceTitle])}
|
||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -106,7 +106,7 @@ class RemoveQueueItemModal extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
|
||||
@@ -94,7 +94,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
@@ -155,7 +155,7 @@ class AddNewMovie extends Component {
|
||||
!isFetching && !error && !items.length && !!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noResults}>
|
||||
{translate('CouldNotFindResults', [term])}
|
||||
{translate('CouldNotFindResults', { term })}
|
||||
</div>
|
||||
<div>
|
||||
{translate('YouCanAlsoSearch')}
|
||||
|
||||
@@ -119,7 +119,7 @@ class ImportMovie extends Component {
|
||||
rootFoldersPopulated &&
|
||||
!unmappedFolders.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('AllMoviesInPathHaveBeenImported', [path])}
|
||||
{translate('AllMoviesInPathHaveBeenImported', { path })}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -18,17 +18,17 @@ import styles from './ImportMovieSelectFolder.css';
|
||||
const rootFolderColumns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: translate('Path'),
|
||||
label: () => translate('Path'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: translate('FreeSpace'),
|
||||
label: () => translate('FreeSpace'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'unmappedFolders',
|
||||
label: translate('UnmappedFolders'),
|
||||
label: () => translate('UnmappedFolders'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
|
||||
import ApplyTheme from './ApplyTheme';
|
||||
import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history, hasTranslationsError }) {
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title={window.Radarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme>
|
||||
<PageConnector hasTranslationsError={hasTranslationsError}>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
@@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) {
|
||||
|
||||
App.propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
hasTranslationsError: PropTypes.bool.isRequired
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import RootFolderAppState from './RootFolderAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
@@ -39,14 +44,19 @@ export interface CustomFilter {
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
movieFiles: MovieFilesAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
movieIndex: MovieIndexAppState;
|
||||
parse: ParseAppState;
|
||||
settings: SettingsAppState;
|
||||
movies: MoviesAppState;
|
||||
tags: TagsAppState;
|
||||
parse: ParseAppState;
|
||||
queue: QueueAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
|
||||
9
frontend/src/App/State/CalendarAppState.ts
Normal file
9
frontend/src/App/State/CalendarAppState.ts
Normal file
@@ -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;
|
||||
6
frontend/src/App/State/CommandAppState.ts
Normal file
6
frontend/src/App/State/CommandAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
||||
6
frontend/src/App/State/MovieCollectionAppState.ts
Normal file
6
frontend/src/App/State/MovieCollectionAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import MovieCollection from 'typings/MovieCollection';
|
||||
|
||||
type MovieCollectionAppState = AppSectionState<MovieCollection>;
|
||||
|
||||
export default MovieCollectionAppState;
|
||||
@@ -22,6 +22,9 @@ export interface MovieIndexAppState {
|
||||
showQualityProfile: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showCinemaRelease: boolean;
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
|
||||
12
frontend/src/App/State/RootFolderAppState.ts
Normal file
12
frontend/src/App/State/RootFolderAppState.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import RootFolder from 'typings/RootFolder';
|
||||
|
||||
interface RootFolderAppState
|
||||
extends AppSectionState<RootFolder>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export default RootFolderAppState;
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
@@ -35,16 +36,16 @@ export interface QualityProfilesAppState
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexers: IndexerAppState;
|
||||
languages: LanguageSettingsAppState;
|
||||
notifications: NotificationAppState;
|
||||
language: LanguageSettingsAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
10
frontend/src/App/State/SystemAppState.ts
Normal file
10
frontend/src/App/State/SystemAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
|
||||
interface SystemAppState {
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
@@ -1,12 +1,32 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
export interface TagDetail extends ModelBase {
|
||||
label: string;
|
||||
autoTagIds: number[];
|
||||
delayProfileIds: number[];
|
||||
downloadClientIds: number[];
|
||||
importListIds: number[];
|
||||
indexerIds: number[];
|
||||
movieIds: number[];
|
||||
notificationIds: number[];
|
||||
restrictionIds: number[];
|
||||
}
|
||||
|
||||
export interface TagDetailAppState
|
||||
extends AppSectionState<TagDetail>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
|
||||
details: TagDetailAppState;
|
||||
}
|
||||
|
||||
export default TagsAppState;
|
||||
|
||||
56
frontend/src/Calendar/CalendarFilterModal.tsx
Normal file
56
frontend/src/Calendar/CalendarFilterModal.tsx
Normal file
@@ -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 translate from 'Utilities/String/translate';
|
||||
import CalendarConnector from './CalendarConnector';
|
||||
import CalendarFilterModal from './CalendarFilterModal';
|
||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||
import LegendConnector from './Legend/LegendConnector';
|
||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||
@@ -83,6 +84,7 @@ class CalendarPage extends Component {
|
||||
movieIsFetching,
|
||||
movieIsPopulated,
|
||||
missingMovieIds,
|
||||
customFilters,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing,
|
||||
useCurrentPage,
|
||||
@@ -137,7 +139,8 @@ class CalendarPage extends Component {
|
||||
isDisabled={!hasMovie}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={CalendarFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
@@ -208,6 +211,7 @@ CalendarPage.propTypes = {
|
||||
movieIsFetching: PropTypes.bool.isRequired,
|
||||
movieIsPopulated: PropTypes.bool.isRequired,
|
||||
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
isSearchingForMissing: PropTypes.bool.isRequired,
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
|
||||
@@ -59,6 +60,7 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.calendar.selectedFilterKey,
|
||||
(state) => state.calendar.filters,
|
||||
createCustomFiltersSelector('calendar'),
|
||||
createMovieCountSelector(),
|
||||
createUISettingsSelector(),
|
||||
createMissingMovieIdsSelector(),
|
||||
@@ -67,6 +69,7 @@ function createMapStateToProps() {
|
||||
(
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
movieCount,
|
||||
uiSettings,
|
||||
missingMovieIds,
|
||||
@@ -76,6 +79,7 @@ function createMapStateToProps() {
|
||||
return {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
hasMovie: !!movieCount.count,
|
||||
movieError: movieCount.error,
|
||||
|
||||
@@ -258,7 +258,7 @@ CollectionOverviews.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -14,9 +14,24 @@ import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{ key: 'small', value: translate('Small') },
|
||||
{ key: 'medium', value: translate('Medium') },
|
||||
{ key: 'large', value: translate('Large') }
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
return translate('Small');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
get value() {
|
||||
return translate('Medium');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'large',
|
||||
get value() {
|
||||
return translate('Large');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class CollectionOverviewOptionsModalContent extends Component {
|
||||
|
||||
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
titleClassName,
|
||||
descriptionClassName,
|
||||
title,
|
||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<DescriptionListItemTitle
|
||||
className={titleClassName}
|
||||
>
|
||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
||||
}
|
||||
|
||||
DescriptionListItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
titleClassName: PropTypes.string,
|
||||
descriptionClassName: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
|
||||
@@ -20,12 +20,12 @@ import styles from './FileBrowserModalContent.css';
|
||||
const columns = [
|
||||
{
|
||||
name: 'type',
|
||||
label: translate('Type'),
|
||||
label: () => translate('Type'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: translate('Name'),
|
||||
label: () => translate('Name'),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -10,12 +10,42 @@ import { NAME } from './FilterBuilderRowValue';
|
||||
import styles from './DateFilterBuilderRowValue.css';
|
||||
|
||||
const timeOptions = [
|
||||
{ key: 'seconds', value: translate('Seconds') },
|
||||
{ key: 'minutes', value: translate('Minutes') },
|
||||
{ key: 'hours', value: translate('Hours') },
|
||||
{ key: 'days', value: translate('Days') },
|
||||
{ key: 'weeks', value: translate('Weeks') },
|
||||
{ key: 'months', value: translate('Months') }
|
||||
{
|
||||
key: 'seconds',
|
||||
get value() {
|
||||
return translate('Seconds');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'minutes',
|
||||
get value() {
|
||||
return translate('Minutes');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'hours',
|
||||
get value() {
|
||||
return translate('Hours');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'days',
|
||||
get value() {
|
||||
return translate('Days');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'weeks',
|
||||
get value() {
|
||||
return translate('Weeks');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'months',
|
||||
get value() {
|
||||
return translate('Months');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function isInFilter(filterType) {
|
||||
|
||||
@@ -210,11 +210,13 @@ class FilterBuilderRow extends Component {
|
||||
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
|
||||
|
||||
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
||||
const { name, label } = availablePropFilter;
|
||||
|
||||
return {
|
||||
key: availablePropFilter.name,
|
||||
value: availablePropFilter.label
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
});
|
||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
|
||||
@@ -3,9 +3,24 @@ import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: 'announced', name: translate('Announced') },
|
||||
{ id: 'inCinemas', name: translate('InCinemas') },
|
||||
{ id: 'released', name: translate('Released') }
|
||||
{
|
||||
id: 'announced',
|
||||
get name() {
|
||||
return translate('Announced');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'inCinemas',
|
||||
get name() {
|
||||
return translate('InCinemas');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'released',
|
||||
get name() {
|
||||
return translate('Released');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function MinimumAvailabilityFilterBuilderRowValue(props) {
|
||||
|
||||
@@ -4,10 +4,30 @@ import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: 'tba', name: 'TBA' },
|
||||
{ id: 'announced', name: translate('Announced') },
|
||||
{ id: 'inCinemas', name: translate('InCinemas') },
|
||||
{ id: 'released', name: translate('Released') },
|
||||
{ id: 'deleted', name: translate('Deleted') }
|
||||
{
|
||||
id: 'announced',
|
||||
get name() {
|
||||
return translate('Announced');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'inCinemas',
|
||||
get name() {
|
||||
return translate('InCinemas');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'released',
|
||||
get name() {
|
||||
return translate('Released');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'deleted',
|
||||
get name() {
|
||||
return translate('Deleted');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function ReleaseStatusFilterBuilderRowValue(props) {
|
||||
|
||||
@@ -4,9 +4,24 @@ import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const availabilityOptions = [
|
||||
{ key: 'announced', value: translate('Announced') },
|
||||
{ key: 'inCinemas', value: translate('InCinemas') },
|
||||
{ key: 'released', value: translate('Released') }
|
||||
{
|
||||
key: 'announced',
|
||||
get value() {
|
||||
return translate('Announced');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'inCinemas',
|
||||
get value() {
|
||||
return translate('InCinemas');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'released',
|
||||
get value() {
|
||||
return translate('Released');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props) {
|
||||
@@ -20,7 +35,7 @@ function AvailabilitySelectInput(props) {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||
@@ -65,6 +66,9 @@ function getComponent(type) {
|
||||
case inputTypes.QUALITY_PROFILE_SELECT:
|
||||
return QualityProfileSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.MOVIE_MONITORED_SELECT:
|
||||
return MovieMonitoredSelectInput;
|
||||
|
||||
|
||||
93
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
93
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
@@ -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);
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function MovieMonitoredSelectInput(props) {
|
||||
@@ -14,7 +15,7 @@ function MovieMonitoredSelectInput(props) {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class NumberInput extends Component {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { value } = this.props;
|
||||
|
||||
if (value !== prevProps.value && !this.state.isFocused) {
|
||||
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
|
||||
this.setState({
|
||||
value: value == null ? '' : value.toString()
|
||||
});
|
||||
|
||||
@@ -35,6 +35,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||
return inputTypes.TEXT;
|
||||
case 'oAuth':
|
||||
return inputTypes.OAUTH;
|
||||
case 'rootFolder':
|
||||
return inputTypes.ROOT_FOLDER_SELECT;
|
||||
default:
|
||||
return inputTypes.TEXT;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -24,7 +25,7 @@ function createMapStateToProps() {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import RootFolderSelectInput from './RootFolderSelectInput';
|
||||
|
||||
const ADD_NEW_KEY = 'addNew';
|
||||
@@ -27,7 +28,7 @@ function createMapStateToProps() {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
value: translate('NoChange'),
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
isMissing: false
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ class SelectInput extends Component {
|
||||
value={key}
|
||||
{...otherOptionProps}
|
||||
>
|
||||
{optionValue}
|
||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
disabledClassName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
|
||||
@@ -75,6 +75,18 @@ class TagInput extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTagEdit = ({ value, ...otherProps }) => {
|
||||
const currentValue = this.state.value;
|
||||
|
||||
if (currentValue && this.props.onTagReplace) {
|
||||
this.props.onTagReplace(otherProps, { name: currentValue });
|
||||
} else {
|
||||
this.props.onTagDelete(otherProps);
|
||||
}
|
||||
|
||||
this.setState({ value });
|
||||
};
|
||||
|
||||
onInputContainerPress = () => {
|
||||
this._autosuggestRef.input.focus();
|
||||
};
|
||||
@@ -188,6 +200,7 @@ class TagInput extends Component {
|
||||
const {
|
||||
tags,
|
||||
kind,
|
||||
canEdit,
|
||||
tagComponent,
|
||||
onTagDelete
|
||||
} = this.props;
|
||||
@@ -199,8 +212,10 @@ class TagInput extends Component {
|
||||
kind={kind}
|
||||
inputProps={inputProps}
|
||||
isFocused={this.state.isFocused}
|
||||
canEdit={canEdit}
|
||||
tagComponent={tagComponent}
|
||||
onTagDelete={onTagDelete}
|
||||
onTagEdit={this.onTagEdit}
|
||||
onInputContainerPress={this.onInputContainerPress}
|
||||
/>
|
||||
);
|
||||
@@ -223,7 +238,7 @@ class TagInput extends Component {
|
||||
<AutoSuggestInput
|
||||
{...otherProps}
|
||||
forwardedRef={this._setAutosuggestRef}
|
||||
className={styles.internalInput}
|
||||
className={className}
|
||||
inputContainerClassName={classNames(
|
||||
inputContainerClassName,
|
||||
isFocused && styles.isFocused
|
||||
@@ -258,11 +273,13 @@ TagInput.propTypes = {
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
minQueryLength: PropTypes.number.isRequired,
|
||||
canEdit: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
tagComponent: PropTypes.elementType.isRequired,
|
||||
onTagAdd: PropTypes.func.isRequired,
|
||||
onTagDelete: PropTypes.func.isRequired
|
||||
onTagDelete: PropTypes.func.isRequired,
|
||||
onTagReplace: PropTypes.func
|
||||
};
|
||||
|
||||
TagInput.defaultProps = {
|
||||
@@ -273,6 +290,7 @@ TagInput.defaultProps = {
|
||||
placeholder: '',
|
||||
delimiters: ['Tab', 'Enter', ' ', ','],
|
||||
minQueryLength: 1,
|
||||
canEdit: false,
|
||||
tagComponent: TagInputTag
|
||||
};
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ class TagInputConnector extends Component {
|
||||
<TagInput
|
||||
onTagAdd={this.onTagAdd}
|
||||
onTagDelete={this.onTagDelete}
|
||||
onTagReplace={this.onTagReplace}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -28,8 +28,10 @@ class TagInputInput extends Component {
|
||||
tags,
|
||||
inputProps,
|
||||
kind,
|
||||
canEdit,
|
||||
tagComponent: TagComponent,
|
||||
onTagDelete
|
||||
onTagDelete,
|
||||
onTagEdit
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -46,8 +48,10 @@ class TagInputInput extends Component {
|
||||
index={index}
|
||||
tag={tag}
|
||||
kind={kind}
|
||||
canEdit={canEdit}
|
||||
isLastTag={index === tags.length - 1}
|
||||
onDelete={onTagDelete}
|
||||
onEdit={onTagEdit}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -66,8 +70,10 @@ TagInputInput.propTypes = {
|
||||
inputProps: PropTypes.object.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
isFocused: PropTypes.bool.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
tagComponent: PropTypes.elementType.isRequired,
|
||||
onTagDelete: PropTypes.func.isRequired,
|
||||
onTagEdit: PropTypes.func.isRequired,
|
||||
onInputContainerPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
.tag {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
max-width: 100%;
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.linkWithEdit {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
max-width: calc(100% - 9px - 4px - 2px);
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.editContainer {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
padding-left: 2px;
|
||||
border-left: 1px solid #eee;
|
||||
}
|
||||
|
||||
.editButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'editButton': string;
|
||||
'editContainer': string;
|
||||
'label': string;
|
||||
'link': string;
|
||||
'linkWithEdit': string;
|
||||
'tag': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import styles from './TagInputTag.css';
|
||||
|
||||
@@ -24,24 +25,61 @@ class TagInputTag extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
onEdit = () => {
|
||||
const {
|
||||
index,
|
||||
tag,
|
||||
onEdit
|
||||
} = this.props;
|
||||
|
||||
onEdit({
|
||||
index,
|
||||
id: tag.id,
|
||||
value: tag.name
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
tag,
|
||||
kind
|
||||
kind,
|
||||
canEdit
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
<div
|
||||
className={styles.tag}
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
<Label kind={kind}>
|
||||
{tag.name}
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kind}
|
||||
>
|
||||
<Link
|
||||
className={canEdit ? styles.linkWithEdit : styles.link}
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
{tag.name}
|
||||
</Link>
|
||||
|
||||
{
|
||||
canEdit ?
|
||||
<div className={styles.editContainer}>
|
||||
<IconButton
|
||||
className={styles.editButton}
|
||||
name={icons.EDIT}
|
||||
size={9}
|
||||
onPress={this.onEdit}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</Label>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +88,9 @@ TagInputTag.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
tag: PropTypes.shape(tagShape),
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
onDelete: PropTypes.func.isRequired
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TagInputTag;
|
||||
|
||||
@@ -46,13 +46,13 @@ class TextTagInputConnector extends Component {
|
||||
// to oddities with restrictions (as an example).
|
||||
|
||||
const newValue = [...valueArray];
|
||||
const newTags = split(tag.name);
|
||||
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
|
||||
|
||||
newTags.forEach((newTag) => {
|
||||
newValue.push(newTag.trim());
|
||||
});
|
||||
|
||||
onChange({ name, value: newValue.join(',') });
|
||||
onChange({ name, value: newValue });
|
||||
};
|
||||
|
||||
onTagDelete = ({ index }) => {
|
||||
@@ -67,10 +67,24 @@ class TextTagInputConnector extends Component {
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue.join(',')
|
||||
value: newValue
|
||||
});
|
||||
};
|
||||
|
||||
onTagReplace = (tagToReplace, newTag) => {
|
||||
const {
|
||||
name,
|
||||
valueArray,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = [...valueArray];
|
||||
newValue.splice(tagToReplace.index, 1);
|
||||
newValue.push(newTag.name.trim());
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -80,6 +94,7 @@ class TextTagInputConnector extends Component {
|
||||
tagList={[]}
|
||||
onTagAdd={this.onTagAdd}
|
||||
onTagDelete={this.onTagDelete}
|
||||
onTagReplace={this.onTagReplace}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
|
||||
return (
|
||||
<span
|
||||
className={containerClassName}
|
||||
title={title}
|
||||
title={typeof title === 'function' ? title() : title}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
@@ -58,7 +58,7 @@ Icon.propTypes = {
|
||||
name: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
fixedWidth: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Link from './Link';
|
||||
import styles from './IconButton.css';
|
||||
|
||||
@@ -23,7 +24,7 @@ function IconButton(props) {
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
aria-label="Table Options Button"
|
||||
aria-label={translate('TableOptionsButton')}
|
||||
isDisabled={isDisabled}
|
||||
{...otherProps}
|
||||
>
|
||||
|
||||
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
kind,
|
||||
isSpinning,
|
||||
error,
|
||||
children,
|
||||
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
|
||||
const showIcon = wasSuccessful || hasWarning || hasError;
|
||||
|
||||
let iconName = icons.CHECK;
|
||||
let iconKind = kinds.SUCCESS;
|
||||
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
|
||||
|
||||
if (hasWarning) {
|
||||
iconName = icons.WARNING;
|
||||
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
|
||||
|
||||
return (
|
||||
<SpinnerButton
|
||||
kind={kind}
|
||||
isSpinning={isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
|
||||
}
|
||||
|
||||
SpinnerErrorButton.propTypes = {
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
children: PropTypes.node.isRequired
|
||||
|
||||
@@ -13,24 +13,51 @@ class InlineMarkdown extends Component {
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
// For now only replace links
|
||||
// For now only replace links or code blocks (not both)
|
||||
const markdownBlocks = [];
|
||||
if (data) {
|
||||
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
||||
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
||||
|
||||
let endIndex = 0;
|
||||
let match = null;
|
||||
while ((match = regex.exec(data)) !== null) {
|
||||
|
||||
while ((match = linkRegex.exec(data)) !== null) {
|
||||
if (match.index > endIndex) {
|
||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||
}
|
||||
|
||||
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
|
||||
endIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (endIndex !== data.length) {
|
||||
if (endIndex !== data.length && markdownBlocks.length > 0) {
|
||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||
}
|
||||
|
||||
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
|
||||
|
||||
endIndex = 0;
|
||||
match = null;
|
||||
let matchedCode = false;
|
||||
|
||||
while ((match = codeRegex.exec(data)) !== null) {
|
||||
matchedCode = true;
|
||||
|
||||
if (match.index > endIndex) {
|
||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||
}
|
||||
|
||||
markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
|
||||
endIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
|
||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||
}
|
||||
|
||||
if (markdownBlocks.length === 0) {
|
||||
markdownBlocks.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
return <span className={className}>{markdownBlocks}</span>;
|
||||
|
||||
@@ -33,7 +33,7 @@ class FilterMenuContent extends Component {
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
{typeof filter.label === 'function' ? filter.label() : filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ function ErrorPage(props) {
|
||||
const {
|
||||
version,
|
||||
isLocalStorageSupported,
|
||||
hasTranslationsError,
|
||||
translationsError,
|
||||
moviesError,
|
||||
customFiltersError,
|
||||
tagsError,
|
||||
@@ -21,8 +21,8 @@ function ErrorPage(props) {
|
||||
|
||||
if (!isLocalStorageSupported) {
|
||||
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
||||
} else if (hasTranslationsError) {
|
||||
errorMessage = 'Failed to load translations from API';
|
||||
} else if (translationsError) {
|
||||
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
|
||||
} else if (moviesError) {
|
||||
errorMessage = getErrorMessage(moviesError, 'Failed to load movie from API');
|
||||
} else if (customFiltersError) {
|
||||
@@ -55,7 +55,7 @@ function ErrorPage(props) {
|
||||
ErrorPage.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
isLocalStorageSupported: PropTypes.bool.isRequired,
|
||||
hasTranslationsError: PropTypes.bool.isRequired,
|
||||
translationsError: PropTypes.object,
|
||||
moviesError: PropTypes.object,
|
||||
customFiltersError: PropTypes.object,
|
||||
tagsError: PropTypes.object,
|
||||
|
||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||
import SignalRConnector from 'Components/SignalRConnector';
|
||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import PageHeader from './Header/PageHeader';
|
||||
import PageSidebar from './Sidebar/PageSidebar';
|
||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
||||
isSmallScreen,
|
||||
isSidebarVisible,
|
||||
enableColorImpairedMode,
|
||||
authenticationEnabled,
|
||||
onSidebarToggle,
|
||||
onSidebarVisibleChange
|
||||
} = this.props;
|
||||
@@ -109,6 +111,10 @@ class Page extends Component {
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
|
||||
<AuthenticationRequiredModal
|
||||
isOpen={!authenticationEnabled}
|
||||
/>
|
||||
</div>
|
||||
</ColorImpairedContext.Provider>
|
||||
);
|
||||
@@ -124,6 +130,7 @@ Page.propTypes = {
|
||||
isUpdated: PropTypes.bool.isRequired,
|
||||
isDisconnected: PropTypes.bool.isRequired,
|
||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||
authenticationEnabled: PropTypes.bool.isRequired,
|
||||
onResize: PropTypes.func.isRequired,
|
||||
onSidebarToggle: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||
import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions';
|
||||
@@ -11,6 +11,7 @@ import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfil
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import ErrorPage from './ErrorPage';
|
||||
import LoadingPage from './LoadingPage';
|
||||
import Page from './Page';
|
||||
@@ -53,6 +54,7 @@ const selectIsPopulated = createSelector(
|
||||
(state) => state.settings.importLists.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(state) => state.movieCollections.isPopulated,
|
||||
(state) => state.app.translations.isPopulated,
|
||||
(
|
||||
customFiltersIsPopulated,
|
||||
tagsIsPopulated,
|
||||
@@ -62,7 +64,8 @@ const selectIsPopulated = createSelector(
|
||||
indexerFlagsIsPopulated,
|
||||
importListsIsPopulated,
|
||||
systemStatusIsPopulated,
|
||||
movieCollectionsIsPopulated
|
||||
movieCollectionsIsPopulated,
|
||||
translationsIsPopulated
|
||||
) => {
|
||||
return (
|
||||
customFiltersIsPopulated &&
|
||||
@@ -73,7 +76,8 @@ const selectIsPopulated = createSelector(
|
||||
indexerFlagsIsPopulated &&
|
||||
importListsIsPopulated &&
|
||||
systemStatusIsPopulated &&
|
||||
movieCollectionsIsPopulated
|
||||
movieCollectionsIsPopulated &&
|
||||
translationsIsPopulated
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -88,6 +92,7 @@ const selectErrors = createSelector(
|
||||
(state) => state.settings.importLists.error,
|
||||
(state) => state.system.status.error,
|
||||
(state) => state.movieCollections.error,
|
||||
(state) => state.app.translations.error,
|
||||
(
|
||||
customFiltersError,
|
||||
tagsError,
|
||||
@@ -97,7 +102,8 @@ const selectErrors = createSelector(
|
||||
indexerFlagsError,
|
||||
importListsError,
|
||||
systemStatusError,
|
||||
movieCollectionsError
|
||||
movieCollectionsError,
|
||||
translationsError
|
||||
) => {
|
||||
const hasError = !!(
|
||||
customFiltersError ||
|
||||
@@ -108,7 +114,8 @@ const selectErrors = createSelector(
|
||||
indexerFlagsError ||
|
||||
importListsError ||
|
||||
systemStatusError ||
|
||||
movieCollectionsError
|
||||
movieCollectionsError ||
|
||||
translationsError
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -121,7 +128,8 @@ const selectErrors = createSelector(
|
||||
indexerFlagsError,
|
||||
importListsError,
|
||||
systemStatusError,
|
||||
movieCollectionsError
|
||||
movieCollectionsError,
|
||||
translationsError
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -133,18 +141,21 @@ function createMapStateToProps() {
|
||||
selectErrors,
|
||||
selectAppProps,
|
||||
createDimensionsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
(
|
||||
enableColorImpairedMode,
|
||||
isPopulated,
|
||||
errors,
|
||||
app,
|
||||
dimensions
|
||||
dimensions,
|
||||
systemStatus
|
||||
) => {
|
||||
return {
|
||||
...app,
|
||||
...errors,
|
||||
isPopulated,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||
enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
@@ -183,6 +194,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatchFetchStatus() {
|
||||
dispatch(fetchStatus());
|
||||
},
|
||||
dispatchFetchTranslations() {
|
||||
dispatch(fetchTranslations());
|
||||
},
|
||||
onResize(dimensions) {
|
||||
dispatch(saveDimensions(dimensions));
|
||||
},
|
||||
@@ -217,6 +231,7 @@ class PageConnector extends Component {
|
||||
this.props.dispatchFetchImportLists();
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
this.props.dispatchFetchTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +247,6 @@ class PageConnector extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
hasTranslationsError,
|
||||
isPopulated,
|
||||
hasError,
|
||||
dispatchFetchMovies,
|
||||
@@ -244,15 +258,15 @@ class PageConnector extends Component {
|
||||
dispatchFetchImportLists,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
dispatchFetchTranslations,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
|
||||
if (hasError || !this.state.isLocalStorageSupported) {
|
||||
return (
|
||||
<ErrorPage
|
||||
{...this.state}
|
||||
{...otherProps}
|
||||
hasTranslationsError={hasTranslationsError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -273,7 +287,6 @@ class PageConnector extends Component {
|
||||
}
|
||||
|
||||
PageConnector.propTypes = {
|
||||
hasTranslationsError: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool.isRequired,
|
||||
isSidebarVisible: PropTypes.bool.isRequired,
|
||||
@@ -287,6 +300,7 @@ PageConnector.propTypes = {
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -21,24 +21,24 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
|
||||
const links = [
|
||||
{
|
||||
iconName: icons.MOVIE_CONTINUING,
|
||||
title: translate('Movies'),
|
||||
title: () => translate('Movies'),
|
||||
to: '/',
|
||||
alias: '/movies',
|
||||
children: [
|
||||
{
|
||||
title: translate('AddNew'),
|
||||
title: () => translate('AddNew'),
|
||||
to: '/add/new'
|
||||
},
|
||||
{
|
||||
title: translate('ImportLibrary'),
|
||||
title: () => translate('ImportLibrary'),
|
||||
to: '/add/import'
|
||||
},
|
||||
{
|
||||
title: translate('Collections'),
|
||||
title: () => translate('Collections'),
|
||||
to: '/collections'
|
||||
},
|
||||
{
|
||||
title: translate('Discover'),
|
||||
title: () => translate('Discover'),
|
||||
to: '/add/discover'
|
||||
}
|
||||
]
|
||||
@@ -46,26 +46,26 @@ const links = [
|
||||
|
||||
{
|
||||
iconName: icons.CALENDAR,
|
||||
title: translate('Calendar'),
|
||||
title: () => translate('Calendar'),
|
||||
to: '/calendar'
|
||||
},
|
||||
|
||||
{
|
||||
iconName: icons.ACTIVITY,
|
||||
title: translate('Activity'),
|
||||
title: () => translate('Activity'),
|
||||
to: '/activity/queue',
|
||||
children: [
|
||||
{
|
||||
title: translate('Queue'),
|
||||
title: () => translate('Queue'),
|
||||
to: '/activity/queue',
|
||||
statusComponent: QueueStatusConnector
|
||||
},
|
||||
{
|
||||
title: translate('History'),
|
||||
title: () => translate('History'),
|
||||
to: '/activity/history'
|
||||
},
|
||||
{
|
||||
title: translate('Blocklist'),
|
||||
title: () => translate('Blocklist'),
|
||||
to: '/activity/blocklist'
|
||||
}
|
||||
]
|
||||
@@ -73,55 +73,55 @@ const links = [
|
||||
|
||||
{
|
||||
iconName: icons.SETTINGS,
|
||||
title: translate('Settings'),
|
||||
title: () => translate('Settings'),
|
||||
to: '/settings',
|
||||
children: [
|
||||
{
|
||||
title: translate('MediaManagement'),
|
||||
title: () => translate('MediaManagement'),
|
||||
to: '/settings/mediamanagement'
|
||||
},
|
||||
{
|
||||
title: translate('Profiles'),
|
||||
title: () => translate('Profiles'),
|
||||
to: '/settings/profiles'
|
||||
},
|
||||
{
|
||||
title: translate('Quality'),
|
||||
title: () => translate('Quality'),
|
||||
to: '/settings/quality'
|
||||
},
|
||||
{
|
||||
title: translate('CustomFormats'),
|
||||
title: () => translate('CustomFormats'),
|
||||
to: '/settings/customformats'
|
||||
},
|
||||
{
|
||||
title: translate('Indexers'),
|
||||
title: () => translate('Indexers'),
|
||||
to: '/settings/indexers'
|
||||
},
|
||||
{
|
||||
title: translate('DownloadClients'),
|
||||
title: () => translate('DownloadClients'),
|
||||
to: '/settings/downloadclients'
|
||||
},
|
||||
{
|
||||
title: translate('Lists'),
|
||||
title: () => translate('Lists'),
|
||||
to: '/settings/importlists'
|
||||
},
|
||||
{
|
||||
title: translate('Connect'),
|
||||
title: () => translate('Connect'),
|
||||
to: '/settings/connect'
|
||||
},
|
||||
{
|
||||
title: translate('Metadata'),
|
||||
title: () => translate('Metadata'),
|
||||
to: '/settings/metadata'
|
||||
},
|
||||
{
|
||||
title: translate('Tags'),
|
||||
title: () => translate('Tags'),
|
||||
to: '/settings/tags'
|
||||
},
|
||||
{
|
||||
title: translate('General'),
|
||||
title: () => translate('General'),
|
||||
to: '/settings/general'
|
||||
},
|
||||
{
|
||||
title: translate('UI'),
|
||||
title: () => translate('UI'),
|
||||
to: '/settings/ui'
|
||||
}
|
||||
]
|
||||
@@ -129,32 +129,32 @@ const links = [
|
||||
|
||||
{
|
||||
iconName: icons.SYSTEM,
|
||||
title: translate('System'),
|
||||
title: () => translate('System'),
|
||||
to: '/system/status',
|
||||
children: [
|
||||
{
|
||||
title: translate('Status'),
|
||||
title: () => translate('Status'),
|
||||
to: '/system/status',
|
||||
statusComponent: HealthStatusConnector
|
||||
},
|
||||
{
|
||||
title: translate('Tasks'),
|
||||
title: () => translate('Tasks'),
|
||||
to: '/system/tasks'
|
||||
},
|
||||
{
|
||||
title: translate('Backup'),
|
||||
title: () => translate('Backup'),
|
||||
to: '/system/backup'
|
||||
},
|
||||
{
|
||||
title: translate('Updates'),
|
||||
title: () => translate('Updates'),
|
||||
to: '/system/updates'
|
||||
},
|
||||
{
|
||||
title: translate('Events'),
|
||||
title: () => translate('Events'),
|
||||
to: '/system/events'
|
||||
},
|
||||
{
|
||||
title: translate('LogFiles'),
|
||||
title: () => translate('LogFiles'),
|
||||
to: '/system/logs/files'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
|
||||
}
|
||||
|
||||
<span className={isChildItem ? styles.noIcon : null}>
|
||||
{title}
|
||||
{typeof title === 'function' ? title() : title}
|
||||
</span>
|
||||
|
||||
{
|
||||
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
|
||||
|
||||
PageSidebarItem.propTypes = {
|
||||
iconName: PropTypes.object,
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
isActiveParent: PropTypes.bool,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
interface Column {
|
||||
name: string;
|
||||
label: string | React.ReactNode;
|
||||
label: string | PropertyFunction<string> | React.ReactNode;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
isVisible: boolean;
|
||||
|
||||
@@ -107,7 +107,7 @@ function Table(props) {
|
||||
{...getTableHeaderCellProps(otherProps)}
|
||||
{...column}
|
||||
>
|
||||
{column.label}
|
||||
{typeof column.label === 'function' ? column.label() : column.label}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
|
||||
const {
|
||||
className,
|
||||
name,
|
||||
label,
|
||||
columnLabel,
|
||||
isSortable,
|
||||
isVisible,
|
||||
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
|
||||
{...otherProps}
|
||||
component="th"
|
||||
className={className}
|
||||
title={columnLabel}
|
||||
label={typeof label === 'function' ? label() : label}
|
||||
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{children}
|
||||
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
|
||||
TableHeaderCell.propTypes = {
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
columnLabel: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
|
||||
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
isSortable: PropTypes.bool,
|
||||
isVisible: PropTypes.bool,
|
||||
isModifiable: PropTypes.bool,
|
||||
|
||||
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
|
||||
isDisabled={isModifiable === false}
|
||||
onChange={onVisibleChange}
|
||||
/>
|
||||
{label}
|
||||
{typeof label === 'function' ? label() : label}
|
||||
</label>
|
||||
|
||||
{
|
||||
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
|
||||
|
||||
TableOptionsColumn.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
isModifiable: PropTypes.bool.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
|
||||
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
|
||||
|
||||
<TableOptionsColumn
|
||||
name={name}
|
||||
label={label}
|
||||
label={typeof label === 'function' ? label() : label}
|
||||
isVisible={isVisible}
|
||||
isModifiable={isModifiable}
|
||||
index={index}
|
||||
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
|
||||
|
||||
TableOptionsColumnDragSource.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
isModifiable: PropTypes.bool.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
|
||||
@@ -6,47 +6,65 @@ import translate from 'Utilities/String/translate';
|
||||
export const shortcuts = {
|
||||
OPEN_KEYBOARD_SHORTCUTS_MODAL: {
|
||||
key: '?',
|
||||
name: translate('OpenThisModal')
|
||||
get name() {
|
||||
return translate('OpenThisModal');
|
||||
}
|
||||
},
|
||||
|
||||
CLOSE_MODAL: {
|
||||
key: 'Esc',
|
||||
name: translate('CloseCurrentModal')
|
||||
get name() {
|
||||
return translate('CloseCurrentModal');
|
||||
}
|
||||
},
|
||||
|
||||
ACCEPT_CONFIRM_MODAL: {
|
||||
key: 'Enter',
|
||||
name: translate('AcceptConfirmationModal')
|
||||
get name() {
|
||||
return translate('AcceptConfirmationModal');
|
||||
}
|
||||
},
|
||||
|
||||
MOVIE_SEARCH_INPUT: {
|
||||
key: 's',
|
||||
name: translate('FocusSearchBox')
|
||||
get name() {
|
||||
return translate('FocusSearchBox');
|
||||
}
|
||||
},
|
||||
|
||||
SAVE_SETTINGS: {
|
||||
key: 'mod+s',
|
||||
name: translate('SaveSettings')
|
||||
get name() {
|
||||
return translate('SaveSettings');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_TOP: {
|
||||
key: 'mod+home',
|
||||
name: translate('MovieIndexScrollTop')
|
||||
get name() {
|
||||
return translate('MovieIndexScrollTop');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_BOTTOM: {
|
||||
key: 'mod+end',
|
||||
name: translate('MovieIndexScrollBottom')
|
||||
get name() {
|
||||
return translate('MovieIndexScrollBottom');
|
||||
}
|
||||
},
|
||||
|
||||
DETAILS_NEXT: {
|
||||
key: '→',
|
||||
name: translate('MovieDetailsNextMovie')
|
||||
get name() {
|
||||
return translate('MovieDetailsNextMovie');
|
||||
}
|
||||
},
|
||||
|
||||
DETAILS_PREVIOUS: {
|
||||
key: '←',
|
||||
name: translate('MovieDetailsPreviousMovie')
|
||||
get name() {
|
||||
return translate('MovieDetailsPreviousMovie');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ class DiscoverMovieFooter extends Component {
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<DiscoverMovieFooterLabel
|
||||
label={translate('MoviesSelectedInterp', [selectedCount])}
|
||||
label={translate('MoviesSelectedInterp', { count: selectedCount })}
|
||||
isSaving={false}
|
||||
/>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ function NoDiscoverMovie(props) {
|
||||
to="/settings/importlists"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
{translate('AddList')}
|
||||
{translate('AddImportList')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,24 @@ import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{ key: 'small', value: translate('Small') },
|
||||
{ key: 'medium', value: translate('Medium') },
|
||||
{ key: 'large', value: translate('Large') }
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
return translate('Small');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
get value() {
|
||||
return translate('Medium');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'large',
|
||||
get value() {
|
||||
return translate('Large');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||
|
||||
@@ -14,9 +14,24 @@ import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{ key: 'small', value: translate('Small') },
|
||||
{ key: 'medium', value: translate('Medium') },
|
||||
{ key: 'large', value: translate('Large') }
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
return translate('Small');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
get value() {
|
||||
return translate('Medium');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'large',
|
||||
get value() {
|
||||
return translate('Large');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
|
||||
@@ -111,7 +111,7 @@ class DiscoverMovieHeader extends Component {
|
||||
isSortable={isSortable}
|
||||
{...otherProps}
|
||||
>
|
||||
{label}
|
||||
{typeof label === 'function' ? label() : label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
})
|
||||
|
||||
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
enum DownloadProtocol {
|
||||
Unknown = 'unknown',
|
||||
Usenet = 'usenet',
|
||||
Torrent = 'torrent',
|
||||
}
|
||||
|
||||
export default DownloadProtocol;
|
||||
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
||||
@@ -0,0 +1,5 @@
|
||||
.authRequiredAlert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'addCustomFormatMessage': string;
|
||||
'authRequiredAlert': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
153
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
153
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
@@ -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';
|
||||
|
||||
export const ARRAY = 'array';
|
||||
export const CONTAINS = 'contains';
|
||||
export const DATE = 'date';
|
||||
export const EQUAL = 'equal';
|
||||
export const EXACT = 'exact';
|
||||
export const NUMBER = 'number';
|
||||
export const STRING = 'string';
|
||||
|
||||
export const all = [
|
||||
ARRAY,
|
||||
CONTAINS,
|
||||
DATE,
|
||||
EQUAL,
|
||||
EXACT,
|
||||
NUMBER,
|
||||
STRING
|
||||
@@ -20,6 +24,10 @@ export const possibleFilterTypes = {
|
||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
||||
],
|
||||
|
||||
[CONTAINS]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' }
|
||||
],
|
||||
|
||||
[DATE]: [
|
||||
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
||||
@@ -29,6 +37,10 @@ export const possibleFilterTypes = {
|
||||
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
||||
],
|
||||
|
||||
[EQUAL]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' }
|
||||
],
|
||||
|
||||
[EXACT]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
faChevronCircleRight as fasChevronCircleRight,
|
||||
faChevronCircleUp as fasChevronCircleUp,
|
||||
faCircle as fasCircle,
|
||||
faCircleDown as fasCircleDown,
|
||||
faCloud as fasCloud,
|
||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||
faCog as fasCog,
|
||||
@@ -135,6 +136,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
||||
export const CHECK_CIRCLE = fasCheckCircle;
|
||||
export const CHECK_SQUARE = fasSquareCheck;
|
||||
export const CIRCLE = fasCircle;
|
||||
export const CIRCLE_DOWN = fasCircleDown;
|
||||
export const CIRCLE_OUTLINE = farCircle;
|
||||
export const CLEAR = fasTrashAlt;
|
||||
export const CLIPBOARD = fasCopy;
|
||||
|
||||
@@ -5,11 +5,13 @@ export const CHECK = 'check';
|
||||
export const DEVICE = 'device';
|
||||
export const KEY_VALUE_LIST = 'keyValueList';
|
||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||
export const FLOAT = 'float';
|
||||
export const NUMBER = 'number';
|
||||
export const OAUTH = 'oauth';
|
||||
export const PASSWORD = 'password';
|
||||
export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||
export const LANGUAGE_SELECT = 'languageSelect';
|
||||
@@ -31,11 +33,13 @@ export const all = [
|
||||
DEVICE,
|
||||
KEY_VALUE_LIST,
|
||||
MOVIE_MONITORED_SELECT,
|
||||
FLOAT,
|
||||
NUMBER,
|
||||
OAUTH,
|
||||
PASSWORD,
|
||||
PATH,
|
||||
QUALITY_PROFILE_SELECT,
|
||||
INDEXER_SELECT,
|
||||
DOWNLOAD_CLIENT_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
INDEXER_FLAGS_SELECT,
|
||||
|
||||
@@ -25,11 +25,11 @@ import styles from './InteractiveImportSelectFolderModalContent.css';
|
||||
const recentFoldersColumns = [
|
||||
{
|
||||
name: 'folder',
|
||||
label: translate('Folder'),
|
||||
label: () => translate('Folder'),
|
||||
},
|
||||
{
|
||||
name: 'lastUsed',
|
||||
label: translate('LastUsed'),
|
||||
label: () => translate('LastUsed'),
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
@@ -100,7 +100,7 @@ function InteractiveImportSelectFolderModalContent(
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{modalTitle} - {translate('SelectFolder')}
|
||||
{translate('SelectFolderModalTitle', { modalTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
@@ -71,36 +71,36 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: translate('RelativePath'),
|
||||
label: () => translate('RelativePath'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'movie',
|
||||
label: translate('Movie'),
|
||||
label: () => translate('Movie'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: translate('ReleaseGroup'),
|
||||
label: () => translate('ReleaseGroup'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: translate('Quality'),
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: translate('Languages'),
|
||||
label: () => translate('Languages'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: translate('Size'),
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
@@ -108,7 +108,7 @@ const COLUMNS = [
|
||||
name: 'customFormats',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.INTERACTIVE,
|
||||
title: translate('CustomFormat'),
|
||||
title: () => translate('CustomFormat'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
@@ -118,6 +118,7 @@ const COLUMNS = [
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
kind: kinds.DANGER,
|
||||
title: () => translate('Rejections'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
@@ -127,11 +128,17 @@ const COLUMNS = [
|
||||
const importModeOptions = [
|
||||
{
|
||||
key: 'chooseImportMode',
|
||||
value: translate('ChooseImportMode'),
|
||||
value: () => translate('ChooseImportMode'),
|
||||
disabled: true,
|
||||
},
|
||||
{ key: 'move', value: translate('MoveFiles') },
|
||||
{ key: 'copy', value: translate('HardlinkCopyFiles') },
|
||||
{
|
||||
key: 'move',
|
||||
value: () => translate('MoveFiles'),
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
value: () => translate('HardlinkCopyFiles'),
|
||||
},
|
||||
];
|
||||
|
||||
function isSameMovieFile(
|
||||
@@ -236,10 +243,23 @@ function InteractiveImportModalContent(
|
||||
useState<string | null>(null);
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const [bulkSelectOptions, setBulkSelectOptions] = useState([
|
||||
{ key: 'select', value: translate('SelectDotDot'), disabled: true },
|
||||
{ key: 'quality', value: translate('SelectQuality') },
|
||||
{ key: 'releaseGroup', value: translate('SelectReleaseGroup') },
|
||||
{ key: 'language', value: translate('SelectLanguage') },
|
||||
{
|
||||
key: 'select',
|
||||
value: translate('SelectDropdown'),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'quality',
|
||||
value: translate('SelectQuality'),
|
||||
},
|
||||
{
|
||||
key: 'releaseGroup',
|
||||
value: translate('SelectReleaseGroup'),
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
value: translate('SelectLanguage'),
|
||||
},
|
||||
]);
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
const previousIsDeleting = usePrevious(isDeleting);
|
||||
@@ -384,7 +404,9 @@ function InteractiveImportModalContent(
|
||||
const files: InteractiveImportCommandOptions[] = [];
|
||||
|
||||
if (finalImportMode === 'chooseImportMode') {
|
||||
setInteractiveImportErrorMessage('An import mode must be selected');
|
||||
setInteractiveImportErrorMessage(
|
||||
translate('InteractiveImportNoImportMode')
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -397,21 +419,21 @@ function InteractiveImportModalContent(
|
||||
|
||||
if (!movie) {
|
||||
setInteractiveImportErrorMessage(
|
||||
translate('InteractiveImportErrMovie')
|
||||
translate('InteractiveImportNoMovie')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!quality) {
|
||||
setInteractiveImportErrorMessage(
|
||||
translate('InteractiveImportErrQuality')
|
||||
translate('InteractiveImportNoQuality')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!languages) {
|
||||
setInteractiveImportErrorMessage(
|
||||
translate('InteractiveImportErrLanguage')
|
||||
translate('InteractiveImportNoLanguage')
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -599,7 +621,7 @@ function InteractiveImportModalContent(
|
||||
|
||||
const errorMessage = getErrorMessage(
|
||||
error,
|
||||
translate('UnableToLoadManualImportItems')
|
||||
translate('InteractiveImportLoadError')
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -679,7 +701,7 @@ function InteractiveImportModalContent(
|
||||
) : null}
|
||||
|
||||
{isPopulated && !items.length && !isFetching
|
||||
? translate('NoVideoFilesFoundSelectedFolder')
|
||||
? translate('InteractiveImportNoFilesFound')
|
||||
: null}
|
||||
</ModalBody>
|
||||
|
||||
@@ -775,8 +797,8 @@ function InteractiveImportModalContent(
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteSelectedMovieFiles')}
|
||||
message={translate('DeleteSelectedMovieFilesMessage')}
|
||||
confirmLabel="Delete"
|
||||
message={translate('DeleteSelectedMovieFilesHelpText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onConfirmDeleteModalClose}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import Rejection from 'typings/Rejection';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
import styles from './InteractiveImportRow.css';
|
||||
@@ -45,6 +46,7 @@ interface InteractiveImportRowProps {
|
||||
languages?: Language[];
|
||||
size: number;
|
||||
customFormats?: object[];
|
||||
customFormatScore?: number;
|
||||
rejections: Rejection[];
|
||||
columns: Column[];
|
||||
movieFileId?: number;
|
||||
@@ -66,6 +68,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
releaseGroup,
|
||||
size,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
rejections,
|
||||
isSelected,
|
||||
modalTitle,
|
||||
@@ -293,8 +296,11 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
<TableRowCell>
|
||||
{customFormats?.length ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.INTERACTIVE} />}
|
||||
title={translate('Formats')}
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
title={translate('CustomFormats')}
|
||||
body={
|
||||
<div className={styles.customFormatTooltip}>
|
||||
<MovieFormats formats={customFormats} />
|
||||
|
||||
@@ -80,16 +80,14 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{modalTitle} - {translate('SelectLanguage')}
|
||||
{translate('SelectLanguageModalTitle', { modalTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadLanguages')}
|
||||
</Alert>
|
||||
<Alert kind={kinds.DANGER}>{translate('LanguagesLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
|
||||
@@ -29,22 +29,22 @@ import styles from './SelectMovieModalContent.css';
|
||||
const columns = [
|
||||
{
|
||||
name: 'title',
|
||||
label: translate('Title'),
|
||||
label: () => translate('Title'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'year',
|
||||
label: translate('Year'),
|
||||
label: () => translate('Year'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'imdbId',
|
||||
label: translate('ImdbId'),
|
||||
label: () => translate('IMDbId'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'tmdbId',
|
||||
label: translate('TmdbId'),
|
||||
label: () => translate('TMDBId'),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
@@ -166,8 +166,11 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
||||
a.sortTitle.localeCompare(b.sortTitle)
|
||||
);
|
||||
|
||||
return sorted.filter((item) =>
|
||||
item.title.toLowerCase().includes(filter.toLowerCase())
|
||||
return sorted.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.tmdbId.toString().includes(filter) ||
|
||||
item.imdbId?.includes(filter)
|
||||
);
|
||||
}, [allMovies, filter]);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ function SelectMovieModalTableHeader(props: SelectMovieModalTableHeaderProps) {
|
||||
}
|
||||
name={name}
|
||||
>
|
||||
{label}
|
||||
{typeof label === 'function' ? label() : label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -28,7 +28,11 @@ class SelectMovieRow extends Component {
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.imdbId}>
|
||||
<Label>{this.props.imdbId}</Label>
|
||||
{
|
||||
this.props.imdbId ?
|
||||
<Label>{this.props.imdbId}</Label> :
|
||||
null
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.tmdbId}>
|
||||
@@ -43,7 +47,7 @@ SelectMovieRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
imdbId: PropTypes.string.isRequired,
|
||||
imdbId: PropTypes.string,
|
||||
year: PropTypes.number.isRequired,
|
||||
onMovieSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -131,9 +131,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
|
||||
{isFetching && <LoadingIndicator />}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</Alert>
|
||||
<Alert kind={kinds.DANGER}>{translate('QualitiesLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
|
||||
@@ -39,7 +39,7 @@ function SelectReleaseGroupModalContent(
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{modalTitle} - {translate('SetReleaseGroup')}
|
||||
{translate('SetReleaseGroupModalTitle', { modalTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
@@ -62,7 +62,7 @@ function SelectReleaseGroupModalContent(
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button kind={kinds.SUCCESS} onPress={onReleaseGroupSelectWrapper}>
|
||||
{translate('SetReleaseGroup')}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
||||
import styles from './InteractiveSearchContent.css';
|
||||
@@ -13,13 +14,13 @@ import styles from './InteractiveSearchContent.css';
|
||||
const columns = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: translate('Source'),
|
||||
label: () => translate('Source'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
label: translate('Age'),
|
||||
label: () => translate('Age'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
@@ -32,72 +33,81 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, { name: icons.DANGER }),
|
||||
columnLabel: () => translate('Rejections'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections')
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: translate('Title'),
|
||||
label: () => translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: translate('Indexer'),
|
||||
label: () => translate('Indexer'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
label: translate('History'),
|
||||
label: () => translate('History'),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: translate('Size'),
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'peers',
|
||||
label: translate('Peers'),
|
||||
label: () => translate('Peers'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: translate('Language'),
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: translate('Quality'),
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormat',
|
||||
label: translate('Formats'),
|
||||
label: () => translate('Formats'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: () => translate('CustomFormatScore'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: translate('CustomFormatScore')
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, { name: icons.FLAG }),
|
||||
columnLabel: () => translate('IndexerFlags'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
}
|
||||
@@ -119,36 +129,46 @@ function InteractiveSearchContent(props) {
|
||||
onGrabPress
|
||||
} = props;
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
!isFetching && error ?
|
||||
<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}>
|
||||
{translate('NoResultsFound')}
|
||||
</Alert>
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!totalReleasesCount && isPopulated && !items.length &&
|
||||
!!totalReleasesCount && isPopulated && !items.length ?
|
||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||
{translate('AllResultsHiddenFilter')}
|
||||
</Alert>
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
isPopulated && !!items.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
@@ -171,14 +191,16 @@ function InteractiveSearchContent(props) {
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
totalReleasesCount !== items.length && !!items.length &&
|
||||
totalReleasesCount !== items.length && !!items.length ?
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('SomeResultsHiddenFilter')}
|
||||
</Alert>
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
.quality,
|
||||
.customFormat,
|
||||
.language {
|
||||
.languages {
|
||||
composes: cell;
|
||||
}
|
||||
|
||||
.language {
|
||||
.languages {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
}
|
||||
|
||||
.rejected,
|
||||
.indexerFlags,
|
||||
.download {
|
||||
.indexerFlags {
|
||||
composes: cell;
|
||||
|
||||
width: 50px;
|
||||
@@ -70,3 +69,39 @@
|
||||
.blocklist {
|
||||
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;
|
||||
'customFormatScore': string;
|
||||
'download': string;
|
||||
'downloadIcon': string;
|
||||
'history': string;
|
||||
'indexer': string;
|
||||
'indexerFlags': string;
|
||||
'language': string;
|
||||
'interactiveIcon': string;
|
||||
'languages': string;
|
||||
'manualDownloadContent': string;
|
||||
'peers': string;
|
||||
'protocol': 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;
|
||||
376
frontend/src/InteractiveSearch/InteractiveSearchRow.tsx
Normal file
376
frontend/src/InteractiveSearch/InteractiveSearchRow.tsx
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
.downloadClient {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'enabled': string;
|
||||
'restriction': string;
|
||||
'downloadClient': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user