Compare commits
1 Commits
v5.9.0.905
...
import-lis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed2553883 |
@@ -1,13 +0,0 @@
|
||||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Radarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [7878],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,7 @@ dotnet_diagnostic.CA5397.severity = suggestion
|
||||
|
||||
dotnet_diagnostic.SYSLIB0006.severity = none
|
||||
|
||||
[*.{js,html,hbs,less,css,ts,tsx}]
|
||||
[*.{js,html,js,hbs,less,css}]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
12
.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 Discord first'
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
@@ -65,18 +65,18 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Trace Logs? **Not Optional**
|
||||
label: Trace Logs?
|
||||
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 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`
|
||||
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
|
||||
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.
|
||||
- label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue.
|
||||
required: true
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,3 +3,6 @@ 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.
|
||||
|
||||
12
.github/dependabot.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
16
.github/label-actions.yml
vendored
@@ -1,16 +0,0 @@
|
||||
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||
|
||||
'Type: Support':
|
||||
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).
|
||||
close: true
|
||||
close-reason: 'not planned'
|
||||
|
||||
'Status: Logs Needed':
|
||||
comment: >
|
||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files).
|
||||
17
.github/workflows/label-actions.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: 'Label Actions'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v3
|
||||
with:
|
||||
process-only: 'issues'
|
||||
36
.github/workflows/support.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: 'Support requests'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
support:
|
||||
permissions:
|
||||
issues: write # to modify issues
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Type: Support'
|
||||
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)
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
- uses: dessant/support-requests@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Status: Logs Needed'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files).
|
||||
close-issue: false
|
||||
lock-issue: false
|
||||
1
.gitignore
vendored
@@ -126,7 +126,6 @@ coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
||||
7
.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Radarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Radarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
.vscode/tasks.json
vendored
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Radarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Radarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Radarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Logo/1024.png
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 49 KiB |
BIN
Logo/128.png
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
Logo/16.png
|
Before Width: | Height: | Size: 331 B After Width: | Height: | Size: 577 B |
BIN
Logo/256.png
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 10 KiB |
BIN
Logo/32.png
|
Before Width: | Height: | Size: 666 B After Width: | Height: | Size: 1.1 KiB |
BIN
Logo/400.png
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 17 KiB |
BIN
Logo/48.png
|
Before Width: | Height: | Size: 987 B After Width: | Height: | Size: 1.7 KiB |
BIN
Logo/512.png
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 21 KiB |
BIN
Logo/64.png
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
Logo/800.png
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 49 KiB |
BIN
Logo/text256.png
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 32 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/radarr/installation/docker)
|
||||
[](https://wiki.servarr.com/radarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
@@ -35,6 +35,7 @@ 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,28 +9,24 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.9.0'
|
||||
majorVersion: '4.6.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.424'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
dotnetVersion: '6.0.408'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-12'
|
||||
macImage: 'macOS-11'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- develop
|
||||
- master
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/Radarr.Api.*/openapi.json
|
||||
|
||||
pr:
|
||||
branches:
|
||||
@@ -38,7 +34,6 @@ pr:
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Radarr.Api.*/openapi.json
|
||||
|
||||
@@ -166,10 +161,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: UseNode@1
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
version: $(nodeVersion)
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -368,7 +363,7 @@ stages:
|
||||
- bash: |
|
||||
echo "Uploading source maps to sentry"
|
||||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
RELEASENAME="Radarr@${RADARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
|
||||
RELEASENAME="${RADARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
|
||||
sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}"
|
||||
sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
|
||||
sentry-cli releases set-commits --auto "${RELEASENAME}"
|
||||
@@ -541,8 +536,8 @@ stages:
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres14
|
||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
@@ -594,63 +589,7 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
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'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
@@ -736,8 +675,8 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres14
|
||||
displayName: Integration Native LinuxCore with Postgres14 Database
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
@@ -794,70 +733,7 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
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'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
@@ -1089,10 +965,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: UseNode@1
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
version: $(nodeVersion)
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -1116,7 +992,7 @@ stages:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@1
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
@@ -1128,7 +1004,7 @@ stages:
|
||||
cliProjectName: 'RadarrUI'
|
||||
cliProjectVersion: '$(radarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@2
|
||||
- task: SonarCloudAnalyze@1
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
@@ -1205,7 +1081,7 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
@@ -1223,21 +1099,25 @@ stages:
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@2
|
||||
- task: SonarCloudAnalyze@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5
|
||||
- task: reportgenerator@4
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||
publishCodeCoverageResults: true
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: Publish Coverage Report
|
||||
inputs:
|
||||
codeCoverageTool: 'cobertura'
|
||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||
reportDirectory: './CoverageResults/combined/'
|
||||
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
- Analyze
|
||||
- Installer
|
||||
- Unit_Test
|
||||
- Integration
|
||||
- Automation
|
||||
|
||||
15
build.sh
@@ -254,7 +254,7 @@ InstallInno()
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
@@ -392,21 +392,22 @@ then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
|
||||
if [ "$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
|
||||
|
||||
10
docs.sh
@@ -21,21 +21,15 @@ slnFile=src/Radarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Radarr.Console.dll
|
||||
else
|
||||
application=Radarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
|
||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
||||
@@ -359,16 +359,11 @@ module.exports = {
|
||||
],
|
||||
|
||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'no-shadow': 'off',
|
||||
// These should be enabled after cleaning things up
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'prettier/prettier': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
@@ -381,41 +376,7 @@ module.exports = {
|
||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// React Hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
// React
|
||||
'react/function-component-definition': 'error',
|
||||
'react/hook-use-state': 'error',
|
||||
'react/jsx-boolean-value': ['error', 'always'],
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' }
|
||||
],
|
||||
'react/jsx-fragments': 'error',
|
||||
'react/jsx-handler-names': [
|
||||
'error',
|
||||
{
|
||||
eventHandlerPrefix: 'on',
|
||||
eventHandlerPropPrefix: 'on'
|
||||
}
|
||||
],
|
||||
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||
'react/jsx-sort-props': [
|
||||
'error',
|
||||
{
|
||||
callbacksLast: true,
|
||||
noSortAlphabetically: true,
|
||||
reservedFirst: true
|
||||
}
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'react/self-closing-comp': 'error'
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,18 +2,16 @@ const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
|
||||
['@babel/plugin-proposal-optional-chaining', { loose }],
|
||||
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
|
||||
|
||||
// Stage 2
|
||||
'@babel/plugin-transform-export-namespace-from',
|
||||
'@babel/plugin-proposal-export-namespace-from',
|
||||
|
||||
// Stage 3
|
||||
['@babel/plugin-transform-class-properties', { loose }],
|
||||
['@babel/plugin-proposal-class-properties', { loose }],
|
||||
'@babel/plugin-syntax-dynamic-import'
|
||||
],
|
||||
env: {
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = (env) => {
|
||||
},
|
||||
|
||||
entry: {
|
||||
index: 'index.ts'
|
||||
index: 'index.js'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
@@ -66,23 +66,23 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
moduleIds: 'deterministic',
|
||||
chunkIds: isProduction ? 'deterministic' : 'named'
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial',
|
||||
name: 'vendors'
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
experiments: {
|
||||
topLevelAwait: true
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
@@ -90,15 +90,13 @@ module.exports = (env) => {
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
filename: 'Content/styles.css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.ejs',
|
||||
filename: 'index.html',
|
||||
publicPath: '/',
|
||||
inject: false
|
||||
publicPath: '/'
|
||||
}),
|
||||
|
||||
new FileManagerPlugin({
|
||||
@@ -201,7 +199,7 @@ module.exports = (env) => {
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -235,7 +233,6 @@ module.exports = (env) => {
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10240,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -21,7 +19,6 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||
|
||||
class Blocklist extends Component {
|
||||
@@ -38,7 +35,6 @@ class Blocklist extends Component {
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmRemoveModalOpen: false,
|
||||
isConfirmClearModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
@@ -93,19 +89,6 @@ class Blocklist extends Component {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.setState({ isConfirmClearModalOpen: true });
|
||||
};
|
||||
|
||||
onClearBlocklistConfirmed = () => {
|
||||
this.props.onClearBlocklistPress();
|
||||
this.setState({ isConfirmClearModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmClearModalClose = () => {
|
||||
this.setState({ isConfirmClearModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -116,13 +99,10 @@ class Blocklist extends Component {
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlocklistExecuting,
|
||||
onFilterSelect,
|
||||
onClearBlocklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -130,8 +110,7 @@ class Blocklist extends Component {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isConfirmClearModalOpen
|
||||
isConfirmRemoveModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
@@ -151,9 +130,8 @@ class Blocklist extends Component {
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={this.onClearBlocklistPress}
|
||||
onPress={onClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
@@ -167,15 +145,6 @@ class Blocklist extends Component {
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={BlocklistFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
@@ -187,20 +156,16 @@ class Blocklist extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('BlocklistLoadError')}
|
||||
</Alert>
|
||||
<div>
|
||||
{translate('UnableToLoadBlocklist')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey === 'all' ?
|
||||
translate('NoHistoryBlocklist') :
|
||||
translate('BlocklistFilterHasNoItems')
|
||||
}
|
||||
</Alert>
|
||||
<div>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -244,21 +209,11 @@ class Blocklist extends Component {
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('RemoveSelected')}
|
||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmClearModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ClearBlocklist')}
|
||||
message={translate('ClearBlocklistMessageText')}
|
||||
confirmLabel={translate('Clear')}
|
||||
onConfirm={this.onClearBlocklistConfirmed}
|
||||
onCancel={this.onConfirmClearModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -270,15 +225,11 @@ Blocklist.propTypes = {
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlocklistPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
onClearBlocklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blocklist;
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Blocklist from './Blocklist';
|
||||
@@ -14,12 +13,10 @@ import Blocklist from './Blocklist';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blocklist,
|
||||
createCustomFiltersSelector('blocklist'),
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, customFilters, isClearingBlocklistExecuting) => {
|
||||
(blocklist, isClearingBlocklistExecuting) => {
|
||||
return {
|
||||
isClearingBlocklistExecuting,
|
||||
customFilters,
|
||||
...blocklist
|
||||
};
|
||||
}
|
||||
@@ -100,14 +97,6 @@ class BlocklistConnector extends Component {
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setBlocklistFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
|
||||
@@ -116,6 +105,10 @@ class BlocklistConnector extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -129,7 +122,6 @@ class BlocklistConnector extends Component {
|
||||
onPageSelect={this.onPageSelect}
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||
{...this.props}
|
||||
@@ -150,7 +142,6 @@ BlocklistConnector.propTypes = {
|
||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||
removeBlocklistItems: PropTypes.func.isRequired,
|
||||
setBlocklistSort: PropTypes.func.isRequired,
|
||||
setBlocklistFilter: PropTypes.func.isRequired,
|
||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlocklist: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||
|
||||
function createBlocklistSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.blocklist.items,
|
||||
(blocklistItems) => {
|
||||
return blocklistItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.blocklist.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface BlocklistFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||
const sectionItems = useSelector(createBlocklistSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'blocklist';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setBlocklistFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -82,7 +82,7 @@ class BlocklistRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
@@ -104,7 +104,7 @@ class BlocklistRow extends Component {
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguages
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
@@ -7,7 +7,6 @@ import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionList
|
||||
import Link from 'Components/Link/Link';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
@@ -16,7 +15,6 @@ function HistoryDetails(props) {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
data,
|
||||
downloadId,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
@@ -25,11 +23,11 @@ function HistoryDetails(props) {
|
||||
const {
|
||||
indexer,
|
||||
releaseGroup,
|
||||
movieMatchType,
|
||||
customFormatScore,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
downloadId,
|
||||
movieMatchType,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
@@ -47,31 +45,33 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
indexer ?
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
releaseGroup ?
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('ReleaseGroup')}
|
||||
data={releaseGroup}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
customFormatScore && customFormatScore !== '0' ?
|
||||
<DescriptionListItem
|
||||
title={translate('CustomFormatScore')}
|
||||
data={formatCustomFormatScore(customFormatScore)}
|
||||
/> :
|
||||
null
|
||||
!!nzbInfoUrl &&
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
Info URL
|
||||
</DescriptionListItemTitle>
|
||||
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -84,20 +84,6 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
nzbInfoUrl ?
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
{translate('InfoUrl')}
|
||||
</DescriptionListItemTitle>
|
||||
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
downloadClientNameInfo ?
|
||||
<DescriptionListItem
|
||||
@@ -108,30 +94,27 @@ function HistoryDetails(props) {
|
||||
}
|
||||
|
||||
{
|
||||
downloadId ?
|
||||
!!downloadId &&
|
||||
<DescriptionListItem
|
||||
title={translate('GrabId')}
|
||||
title={translate('GrabID')}
|
||||
data={downloadId}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
indexer ?
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title={translate('AgeWhenGrabbed')}
|
||||
data={formatAge(age, ageHours, ageMinutes)}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
publishedDate ?
|
||||
!!publishedDate &&
|
||||
<DescriptionListItem
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -151,21 +134,11 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
downloadId ?
|
||||
<DescriptionListItem
|
||||
title={translate('GrabId')}
|
||||
data={downloadId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
message ?
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -173,7 +146,6 @@ function HistoryDetails(props) {
|
||||
|
||||
if (eventType === 'downloadFolderImported') {
|
||||
const {
|
||||
customFormatScore,
|
||||
droppedPath,
|
||||
importedPath
|
||||
} = data;
|
||||
@@ -187,32 +159,21 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
droppedPath ?
|
||||
!!droppedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Source')}
|
||||
data={droppedPath}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
importedPath ?
|
||||
!!importedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('ImportedTo')}
|
||||
data={importedPath}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
customFormatScore && customFormatScore !== '0' ?
|
||||
<DescriptionListItem
|
||||
title={translate('CustomFormatScore')}
|
||||
data={formatCustomFormatScore(customFormatScore)}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -220,21 +181,20 @@ function HistoryDetails(props) {
|
||||
|
||||
if (eventType === 'movieFileDeleted') {
|
||||
const {
|
||||
reason,
|
||||
customFormatScore
|
||||
reason
|
||||
} = data;
|
||||
|
||||
let reasonMessage = '';
|
||||
|
||||
switch (reason) {
|
||||
case 'Manual':
|
||||
reasonMessage = translate('DeletedReasonManual');
|
||||
reasonMessage = translate('FileWasDeletedByViaUI');
|
||||
break;
|
||||
case 'MissingFromDisk':
|
||||
reasonMessage = translate('DeletedReasonMissingFromDisk');
|
||||
reasonMessage = translate('MissingFromDisk');
|
||||
break;
|
||||
case 'Upgrade':
|
||||
reasonMessage = translate('DeletedReasonUpgrade');
|
||||
reasonMessage = translate('FileWasDeletedByUpgrade');
|
||||
break;
|
||||
default:
|
||||
reasonMessage = '';
|
||||
@@ -251,15 +211,6 @@ function HistoryDetails(props) {
|
||||
title={translate('Reason')}
|
||||
data={reasonMessage}
|
||||
/>
|
||||
|
||||
{
|
||||
customFormatScore && customFormatScore !== '0' ?
|
||||
<DescriptionListItem
|
||||
title={translate('CustomFormatScore')}
|
||||
data={formatCustomFormatScore(customFormatScore)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -311,21 +262,11 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
downloadId ?
|
||||
<DescriptionListItem
|
||||
title={translate('GrabId')}
|
||||
data={downloadId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
message ?
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -346,7 +287,6 @@ HistoryDetails.propTypes = {
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
@@ -15,19 +15,19 @@ import styles from './HistoryDetailsModal.css';
|
||||
function getHeaderTitle(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return translate('Grabbed');
|
||||
return 'Grabbed';
|
||||
case 'downloadFailed':
|
||||
return translate('DownloadFailed');
|
||||
return 'Download Failed';
|
||||
case 'downloadFolderImported':
|
||||
return translate('MovieImported');
|
||||
return 'Movie Imported';
|
||||
case 'movieFileDeleted':
|
||||
return translate('MovieFileDeleted');
|
||||
return 'Movie File Deleted';
|
||||
case 'movieFileRenamed':
|
||||
return translate('MovieFileRenamed');
|
||||
return 'Movie File Renamed';
|
||||
case 'downloadIgnored':
|
||||
return translate('DownloadIgnored');
|
||||
return 'Download Ignored';
|
||||
default:
|
||||
return translate('Unknown');
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ function HistoryDetailsModal(props) {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
@@ -60,7 +59,6 @@ function HistoryDetailsModal(props) {
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
@@ -75,7 +73,7 @@ function HistoryDetailsModal(props) {
|
||||
isSpinning={isMarkingAsFailed}
|
||||
onPress={onMarkAsFailedPress}
|
||||
>
|
||||
{translate('MarkAsFailed')}
|
||||
Mark as Failed
|
||||
</SpinnerButton>
|
||||
}
|
||||
|
||||
@@ -95,7 +93,6 @@ HistoryDetailsModal.propTypes = {
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -12,9 +11,8 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryFilterModal from './HistoryFilterModal';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
class History extends Component {
|
||||
@@ -34,7 +32,6 @@ class History extends Component {
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
onFilterSelect,
|
||||
onFirstPagePress,
|
||||
@@ -72,8 +69,7 @@ class History extends Component {
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={HistoryFilterModal}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
@@ -87,9 +83,9 @@ class History extends Component {
|
||||
|
||||
{
|
||||
!isFetchingAny && hasError &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('HistoryLoadError')}
|
||||
</Alert>
|
||||
<div>
|
||||
{translate('UnableToLoadHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -97,9 +93,9 @@ class History extends Component {
|
||||
// wait for the episodes to populate because they are never coming.
|
||||
|
||||
isPopulated && !hasError && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoHistoryFound')}
|
||||
</Alert>
|
||||
<div>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -147,9 +143,8 @@ History.propTypes = {
|
||||
moviesError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onFirstPagePress: PropTypes.func.isRequired
|
||||
|
||||
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
|
||||
@@ -12,13 +11,11 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.movies,
|
||||
createCustomFiltersSelector('history'),
|
||||
(history, movies, customFilters) => {
|
||||
(history, movies) => {
|
||||
return {
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
...history
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryEventTypeCell.css';
|
||||
|
||||
function getIconName(eventType, data) {
|
||||
function getIconName(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return icons.DOWNLOADING;
|
||||
@@ -17,7 +16,7 @@ function getIconName(eventType, data) {
|
||||
case 'downloadFailed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'movieFileDeleted':
|
||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||
return icons.DELETE;
|
||||
case 'movieFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
@@ -39,26 +38,26 @@ function getIconKind(eventType) {
|
||||
function getTooltip(eventType, data) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
|
||||
return `Movie grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
|
||||
case 'movieFolderImported':
|
||||
return translate('MovieFolderImportedTooltip');
|
||||
return 'Movie imported from movie folder';
|
||||
case 'downloadFolderImported':
|
||||
return translate('MovieImportedTooltip');
|
||||
return 'Movie downloaded successfully and picked up from download client';
|
||||
case 'downloadFailed':
|
||||
return translate('MovieDownloadFailedTooltip');
|
||||
return 'Movie download failed';
|
||||
case 'movieFileDeleted':
|
||||
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
|
||||
return 'Movie file deleted';
|
||||
case 'movieFileRenamed':
|
||||
return translate('MovieFileRenamedTooltip');
|
||||
return 'Movie file renamed';
|
||||
case 'downloadIgnored':
|
||||
return translate('MovieDownloadIgnoredTooltip');
|
||||
return 'Movie Download Ignored';
|
||||
default:
|
||||
return translate('UnknownEventTooltip');
|
||||
return 'Unknown event';
|
||||
}
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, data }) {
|
||||
const iconName = getIconName(eventType, data);
|
||||
const iconName = getIconName(eventType);
|
||||
const iconKind = getIconKind(eventType);
|
||||
const tooltip = getTooltip(eventType, data);
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface HistoryFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'history';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
@@ -64,7 +63,6 @@ class HistoryRow extends Component {
|
||||
sourceTitle,
|
||||
date,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed,
|
||||
columns,
|
||||
shortDateFormat,
|
||||
@@ -99,7 +97,7 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
@@ -113,7 +111,7 @@ class HistoryRow extends Component {
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguages
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@@ -178,14 +176,7 @@ class HistoryRow extends Component {
|
||||
key={name}
|
||||
className={styles.customFormatScore}
|
||||
>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
{formatCustomFormatScore(customFormatScore)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -217,12 +208,10 @@ class HistoryRow extends Component {
|
||||
key={name}
|
||||
className={styles.details}
|
||||
>
|
||||
<div className={styles.actionContents}>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -236,7 +225,6 @@ class HistoryRow extends Component {
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
@@ -261,7 +249,6 @@ HistoryRow.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
isMarkingAsFailed: PropTypes.bool,
|
||||
markAsFailedError: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
@@ -270,8 +257,4 @@ HistoryRow.propTypes = {
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
HistoryRow.defaultProps = {
|
||||
customFormats: []
|
||||
};
|
||||
|
||||
export default HistoryRow;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
@@ -14,7 +12,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -22,10 +20,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -155,16 +152,11 @@ class Queue extends Component {
|
||||
isMoviesPopulated,
|
||||
moviesError,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
count,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -219,7 +211,6 @@ class Queue extends Component {
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
maxPageSize={200}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
@@ -228,15 +219,6 @@ class Queue extends Component {
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
@@ -249,21 +231,17 @@ class Queue extends Component {
|
||||
|
||||
{
|
||||
!isRefreshing && hasError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('QueueLoadError')}
|
||||
</Alert> :
|
||||
<div>
|
||||
{translate('FailedToLoadQueue')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey !== 'all' && count > 0 ?
|
||||
translate('QueueFilterHasNoItems') :
|
||||
translate('QueueIsEmpty')
|
||||
}
|
||||
</Alert> :
|
||||
<div>
|
||||
{translate('QueueIsEmpty')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -308,16 +286,9 @@ class Queue extends Component {
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
@@ -325,7 +296,7 @@ class Queue extends Component {
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
@@ -353,22 +324,13 @@ Queue.propTypes = {
|
||||
moviesError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Queue.defaultProps = {
|
||||
count: 0
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
@@ -16,16 +15,12 @@ function createMapStateToProps() {
|
||||
(state) => state.movies,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
(state) => state.queue.status.item,
|
||||
createCustomFiltersSelector('queue'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
@@ -111,10 +106,6 @@ class QueueConnector extends Component {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setQueueFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
|
||||
@@ -149,7 +140,6 @@ class QueueConnector extends Component {
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||
@@ -172,7 +162,6 @@ QueueConnector.propTypes = {
|
||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||
gotoQueuePage: PropTypes.func.isRequired,
|
||||
setQueueSort: PropTypes.func.isRequired,
|
||||
setQueueFilter: PropTypes.func.isRequired,
|
||||
setQueueTableOption: PropTypes.func.isRequired,
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.progressBarContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'progressBarContainer': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,71 +1,116 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import styles from './QueueDetails.css';
|
||||
|
||||
function QueueDetails(props) {
|
||||
const {
|
||||
title,
|
||||
size,
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
progressBar
|
||||
} = props;
|
||||
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
const isDownloading = status === 'downloading';
|
||||
const isPaused = status === 'paused';
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
if (
|
||||
(isDownloading || isPaused) &&
|
||||
!hasWarning &&
|
||||
!hasError
|
||||
) {
|
||||
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||
|
||||
if (progress < 5) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
title={`${state} - ${progress.toFixed(1)}% ${title}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return (
|
||||
<Popover
|
||||
className={styles.progressBarContainer}
|
||||
anchor={progressBar}
|
||||
title={`${state} - ${progress.toFixed(1)}%`}
|
||||
body={
|
||||
<div>{title}</div>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
<Icon
|
||||
name={icons.PENDING}
|
||||
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueueStatus
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
if (status === 'completed') {
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ImportFailedInterp', [errorMessage])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadStatus === 'warning') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('UnableToImportCheckLogs')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={`${translate('Downloaded')} - ${translate('WaitingToImport')}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={`${translate('Downloaded')} - ${translate('Importing')}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DownloadFailedInterp', [errorMessage])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (progress < 5) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
QueueDetails.propTypes = {
|
||||
@@ -76,14 +121,8 @@ QueueDetails.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
QueueDetails.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading'
|
||||
};
|
||||
|
||||
export default QueueDetails;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||
|
||||
function createQueueSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface QueueFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||
const sectionItems = useSelector(createQueueSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'queue';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setQueueFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class QueueOptions extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownMovieItems"
|
||||
value={includeUnknownMovieItems}
|
||||
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||
helpText={translate('IncludeUnknownMovieItemsHelpText')}
|
||||
onChange={this.onOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -16,15 +16,8 @@
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.customFormatScore {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'customFormatScore': string;
|
||||
'progress': string;
|
||||
'protocol': string;
|
||||
'quality': string;
|
||||
|
||||
@@ -4,19 +4,17 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
@@ -44,14 +42,14 @@ class QueueRow extends Component {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
||||
onRemoveQueueItemModalConfirmed = (blocklist) => {
|
||||
const {
|
||||
onRemoveQueueItemPress,
|
||||
onQueueRowModalOpenOrClose
|
||||
} = this.props;
|
||||
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
onRemoveQueueItemPress(blocklist, skipRedownload);
|
||||
onRemoveQueueItemPress(blocklist);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
};
|
||||
@@ -90,15 +88,12 @@ class QueueRow extends Component {
|
||||
movie,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
@@ -175,7 +170,7 @@ class QueueRow extends Component {
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguages
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@@ -206,24 +201,6 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.customFormatScore}
|
||||
>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
@@ -317,15 +294,6 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -374,7 +342,6 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -398,15 +365,12 @@ QueueRow.propTypes = {
|
||||
movie: PropTypes.object,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
year: PropTypes.number,
|
||||
@@ -426,7 +390,6 @@ QueueRow.propTypes = {
|
||||
};
|
||||
|
||||
QueueRow.defaultProps = {
|
||||
customFormats: [],
|
||||
isGrabbing: false,
|
||||
isRemoving: false
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.noMessages {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'noMessages': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,167 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueueStatus.css';
|
||||
|
||||
function getDetailedPopoverBody(statusMessages) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={messages.length ? undefined: styles.noMessages}
|
||||
>
|
||||
{title}
|
||||
<ul>
|
||||
{
|
||||
messages.map((message) => {
|
||||
return (
|
||||
<li key={message}>
|
||||
{message}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QueueStatus(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
position,
|
||||
canFlip
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = translate('Downloading');
|
||||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = translate('Paused');
|
||||
}
|
||||
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = translate('Queued');
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = translate('Downloaded');
|
||||
|
||||
if (trackedDownloadState === 'importBlocked') {
|
||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ` - ${translate('Importing')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ` - ${translate('WaitingToProcess')}`;
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = translate('Pending');
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = translate('PendingDownloadClientUnavailable');
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', { warningMessage });
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('ImportFailed', { sourceTitle });
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||
position={position}
|
||||
canFlip={canFlip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatus.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
|
||||
canFlip: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
QueueStatus.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading',
|
||||
canFlip: false
|
||||
};
|
||||
|
||||
export default QueueStatus;
|
||||
@@ -1,10 +1,39 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { tooltipPositions } from 'Helpers/Props';
|
||||
import QueueStatus from './QueueStatus';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
function getDetailedPopoverBody(statusMessages) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div key={title}>
|
||||
{title}
|
||||
<ul>
|
||||
{
|
||||
messages.map((message) => {
|
||||
return (
|
||||
<li key={message}>
|
||||
{message}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
@@ -15,16 +44,97 @@ function QueueStatusCell(props) {
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = translate('Downloading');
|
||||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = translate('Paused');
|
||||
}
|
||||
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = translate('Queued');
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = translate('Downloaded');
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ` - ${translate('Importing')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ` - ${translate('WaitingToProcess')}`;
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = translate('Pending');
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `${translate('Pending')} - ${translate('DownloadClientUnavailable')}`;
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', [warningMessage]);
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('ImportFailed', [sourceTitle]);
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.status}>
|
||||
<QueueStatus
|
||||
sourceTitle={sourceTitle}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||
position={tooltipPositions.RIGHT}
|
||||
canFlip={false}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
@@ -40,8 +150,8 @@ QueueStatusCell.propTypes = {
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading'
|
||||
trackedDownloadStatus: translate('Ok'),
|
||||
trackedDownloadState: translate('Downloading')
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
|
||||
150
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('Remove')} - {sourceTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('RemoveFromQueueText', [sourceTitle])}
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
@@ -1,231 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
remove: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
skipRedownload: boolean;
|
||||
}
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
selectedCount?: number;
|
||||
onRemovePress(props: RemovePressProps): void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
selectedCount,
|
||||
onRemovePress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedCount === 1) {
|
||||
return {
|
||||
title: translate('RemoveSelectedItem'),
|
||||
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: translate('RemoveSelectedItems'),
|
||||
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||
selectedCount,
|
||||
}),
|
||||
};
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
hint: multipleSelected
|
||||
? translate('RemoveMultipleFromDownloadClientHint')
|
||||
: translate('RemoveFromDownloadClientHint'),
|
||||
},
|
||||
{
|
||||
key: 'changeCategory',
|
||||
value: translate('ChangeCategory'),
|
||||
isDisabled: !canChangeCategory,
|
||||
hint: multipleSelected
|
||||
? translate('ChangeCategoryMultipleHint')
|
||||
: translate('ChangeCategoryHint'),
|
||||
},
|
||||
{
|
||||
key: 'ignore',
|
||||
value: multipleSelected
|
||||
? translate('IgnoreDownloads')
|
||||
: translate('IgnoreDownload'),
|
||||
isDisabled: !canIgnore,
|
||||
hint: multipleSelected
|
||||
? translate('IgnoreDownloadsHint')
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
hint: translate('DoNotBlocklistHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
isDisabled: isPending,
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [isPending, multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
remove: removalMethod === 'removeFromClient',
|
||||
changeCategory: removalMethod === 'changeCategory',
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>{message}</div>
|
||||
|
||||
{isPending ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="removalMethod"
|
||||
value={removalMethod}
|
||||
values={removalMethodOptions}
|
||||
isDisabled={!canChangeCategory && !canIgnore}
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{multipleSelected
|
||||
? translate('BlocklistReleases')
|
||||
: translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="blocklistMethod"
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
154
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
||||
@@ -1,9 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
@@ -28,13 +25,11 @@ function TimeleftCell(props) {
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={translate('DelayingDownloadUntil', { date, time })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('DelayingDownloadUntilInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -44,13 +39,11 @@ function TimeleftCell(props) {
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={translate('RetryingDownloadOn', { date, time })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('RetryingDownloadInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
@@ -131,12 +130,7 @@ class AddNewMovie extends Component {
|
||||
<div className={styles.helpText}>
|
||||
{translate('FailedLoadingSearchResults')}
|
||||
</div>
|
||||
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
|
||||
<div>
|
||||
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
||||
{translate('WhySearchesCouldBeFailing')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
@@ -161,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')}
|
||||
|
||||
@@ -3,12 +3,10 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
import AddNewMovie from './AddNewMovie';
|
||||
|
||||
@@ -35,10 +33,9 @@ const mapDispatchToProps = {
|
||||
lookupMovie,
|
||||
clearAddMovie,
|
||||
fetchRootFolders,
|
||||
fetchImportExclusions,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails,
|
||||
fetchMovieFiles,
|
||||
clearMovieFiles
|
||||
clearQueueDetails
|
||||
};
|
||||
|
||||
class AddNewMovieConnector extends Component {
|
||||
@@ -54,23 +51,10 @@ class AddNewMovieConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
this.props.fetchImportExclusions();
|
||||
this.props.fetchQueueDetails();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
if (hasDifferentItems(prevProps.items, items)) {
|
||||
const movieIds = selectUniqueIds(items, 'internalId');
|
||||
|
||||
if (movieIds.length) {
|
||||
this.props.fetchMovieFiles({ movieId: movieIds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._movieLookupTimeout) {
|
||||
clearTimeout(this._movieLookupTimeout);
|
||||
@@ -78,7 +62,6 @@ class AddNewMovieConnector extends Component {
|
||||
|
||||
this.props.clearAddMovie();
|
||||
this.props.clearQueueDetails();
|
||||
this.props.clearMovieFiles();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -124,14 +107,12 @@ class AddNewMovieConnector extends Component {
|
||||
|
||||
AddNewMovieConnector.propTypes = {
|
||||
term: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
lookupMovie: PropTypes.func.isRequired,
|
||||
clearAddMovie: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
fetchImportExclusions: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
clearMovieFiles: PropTypes.func.isRequired
|
||||
clearQueueDetails: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
|
||||
|
||||
@@ -85,14 +85,8 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.originalLanguage,
|
||||
.studio,
|
||||
.genres {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-left: 5px;
|
||||
margin-left: 8px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,8 @@ interface CssExports {
|
||||
'certification': string;
|
||||
'content': string;
|
||||
'exclusionIcon': string;
|
||||
'genres': string;
|
||||
'icons': string;
|
||||
'links': string;
|
||||
'originalLanguage': string;
|
||||
'overlay': string;
|
||||
'overview': string;
|
||||
'poster': string;
|
||||
@@ -16,7 +14,6 @@ interface CssExports {
|
||||
'runtime': string;
|
||||
'searchResult': string;
|
||||
'statusContainer': string;
|
||||
'studio': string;
|
||||
'title': string;
|
||||
'titleContainer': string;
|
||||
'titleRow': string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
@@ -62,23 +61,21 @@ class AddNewMovieSearchResult extends Component {
|
||||
titleSlug,
|
||||
year,
|
||||
studio,
|
||||
originalLanguage,
|
||||
genres,
|
||||
status,
|
||||
overview,
|
||||
ratings,
|
||||
folder,
|
||||
images,
|
||||
existingMovieId,
|
||||
isExistingMovie,
|
||||
isExcluded,
|
||||
isExclusionMovie,
|
||||
isSmallScreen,
|
||||
colorImpairedMode,
|
||||
id,
|
||||
monitored,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
movieFile,
|
||||
queueItem,
|
||||
queueStatus,
|
||||
queueState,
|
||||
runtime,
|
||||
movieRuntimeFormat,
|
||||
certification
|
||||
@@ -88,8 +85,6 @@ class AddNewMovieSearchResult extends Component {
|
||||
isNewAddMovieModalOpen
|
||||
} = this.state;
|
||||
|
||||
const hasMovieFile = !!movieFile;
|
||||
|
||||
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
||||
const posterWidth = 167;
|
||||
const posterHeight = 250;
|
||||
@@ -125,13 +120,13 @@ class AddNewMovieSearchResult extends Component {
|
||||
{
|
||||
isExistingMovie &&
|
||||
<MovieIndexProgressBar
|
||||
movieId={existingMovieId}
|
||||
movieFile={movieFile}
|
||||
monitored={monitored}
|
||||
hasFile={hasMovieFile}
|
||||
hasFile={hasFile}
|
||||
status={status}
|
||||
width={posterWidth}
|
||||
detailedProgressBar={true}
|
||||
queueStatus={queueStatus}
|
||||
queueState={queueState}
|
||||
isAvailable={isAvailable}
|
||||
/>
|
||||
}
|
||||
@@ -167,7 +162,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
isExcluded &&
|
||||
isExclusionMovie &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
@@ -203,56 +198,10 @@ class AddNewMovieSearchResult extends Component {
|
||||
</Label>
|
||||
|
||||
{
|
||||
ratings.imdb ?
|
||||
!!studio &&
|
||||
<Label size={sizes.LARGE}>
|
||||
<ImdbRating
|
||||
ratings={ratings}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
originalLanguage?.name ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.LANGUAGE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.originalLanguage}>
|
||||
{originalLanguage.name}
|
||||
</span>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
studio ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.STUDIO}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.studio}>
|
||||
{studio}
|
||||
</span>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
genres.length > 0 ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.GENRE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.genres}>
|
||||
{genres.slice(0, 3).join(', ')}
|
||||
</span>
|
||||
</Label> :
|
||||
null
|
||||
{studio}
|
||||
</Label>
|
||||
}
|
||||
|
||||
<Tooltip
|
||||
@@ -266,15 +215,15 @@ class AddNewMovieSearchResult extends Component {
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
Links
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
<MovieDetailsLinks
|
||||
tmdbId={tmdbId}
|
||||
imdbId={imdbId}
|
||||
youTubeTrailerId={youTubeTrailerId}
|
||||
imdbId={imdbId}
|
||||
/>
|
||||
}
|
||||
canFlip={true}
|
||||
@@ -285,11 +234,9 @@ class AddNewMovieSearchResult extends Component {
|
||||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
hasMovieFiles={hasFile}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
queueItem={queueItem}
|
||||
id={id}
|
||||
useLabel={true}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
@@ -326,31 +273,25 @@ AddNewMovieSearchResult.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
studio: PropTypes.string,
|
||||
originalLanguage: PropTypes.object,
|
||||
genres: PropTypes.arrayOf(PropTypes.string),
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
existingMovieId: PropTypes.number,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
isExcluded: PropTypes.bool,
|
||||
isExclusionMovie: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number,
|
||||
queueItems: PropTypes.arrayOf(PropTypes.object),
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
movieFile: PropTypes.object,
|
||||
queueItem: PropTypes.object,
|
||||
colorImpairedMode: PropTypes.bool,
|
||||
queueStatus: PropTypes.string,
|
||||
queueState: PropTypes.string,
|
||||
runtime: PropTypes.number.isRequired,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
certification: PropTypes.string
|
||||
};
|
||||
|
||||
AddNewMovieSearchResult.defaultProps = {
|
||||
genres: [],
|
||||
isExcluded: false
|
||||
};
|
||||
|
||||
export default AddNewMovieSearchResult;
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
|
||||
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
||||
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingMovieSelector(),
|
||||
createExclusionMovieSelector(),
|
||||
createDimensionsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(state) => state.movieFiles.items,
|
||||
(state, { internalId }) => internalId,
|
||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
|
||||
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
|
||||
|
||||
return {
|
||||
existingMovieId: internalId,
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
queueItem,
|
||||
movieFile,
|
||||
movieRuntimeFormat
|
||||
queueStatus: firstQueueItem ? firstQueueItem.status : null,
|
||||
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { reduce } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
@@ -107,9 +105,9 @@ class ImportMovie extends Component {
|
||||
|
||||
{
|
||||
!rootFoldersFetching && !!rootFoldersError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
<div>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</Alert> :
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -118,9 +116,9 @@ class ImportMovie extends Component {
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersPopulated &&
|
||||
!unmappedFolders.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('AllMoviesInPathHaveBeenImported', { path })}
|
||||
</Alert> :
|
||||
<div>
|
||||
{translate('AllMoviesInPathHaveBeenImported', [path])}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import styles from './ImportMovieRow.css';
|
||||
function ImportMovieRow(props) {
|
||||
const {
|
||||
id,
|
||||
relativePath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
@@ -32,7 +31,7 @@ function ImportMovieRow(props) {
|
||||
/>
|
||||
|
||||
<VirtualTableRowCell className={styles.folder}>
|
||||
{relativePath}
|
||||
{id}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.movie}>
|
||||
@@ -74,7 +73,6 @@ function ImportMovieRow(props) {
|
||||
|
||||
ImportMovieRow.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
minimumAvailability: PropTypes.string.isRequired,
|
||||
|
||||
@@ -30,7 +30,7 @@ class ImportMovieTable extends Component {
|
||||
unmappedFolders.forEach((unmappedFolder) => {
|
||||
const id = unmappedFolder.name;
|
||||
|
||||
onMovieLookup(id, unmappedFolder.path, unmappedFolder.relativePath);
|
||||
onMovieLookup(id, unmappedFolder.path);
|
||||
|
||||
onSetImportMovieValue({
|
||||
id,
|
||||
|
||||
@@ -25,11 +25,10 @@ function createMapStateToProps() {
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onMovieLookup(name, path, relativePath) {
|
||||
onMovieLookup(name, path) {
|
||||
dispatch(queueLookupMovie({
|
||||
name,
|
||||
path,
|
||||
relativePath,
|
||||
term: name
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
.contentContainer {
|
||||
z-index: $popperZIndex;
|
||||
margin-top: 4px;
|
||||
/* 400px container width with 8px padding on each side */
|
||||
/* 400px container witdh with 8px padding on each side */
|
||||
width: 384px;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
@@ -92,9 +92,9 @@ class ImportMovieSelectFolder extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
<div>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</Alert> :
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
|
||||
className={styles.addErrorAlert}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{translate('AddRootFolderError')}
|
||||
{translate('UnableToAddRootFolder')}
|
||||
|
||||
<ul>
|
||||
{
|
||||
|
||||
@@ -5,13 +5,12 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createRootFoldersSelector(),
|
||||
(state) => state.rootFolders,
|
||||
createSystemStatusSelector(),
|
||||
(rootFolders, systemStatus) => {
|
||||
return {
|
||||
|
||||
@@ -12,10 +12,11 @@ function App({ store, history }) {
|
||||
<DocumentTitle title={window.Radarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
||||
@@ -13,7 +13,7 @@ import Switch from 'Components/Router/Switch';
|
||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
|
||||
import MovieIndex from 'Movie/Index/MovieIndex';
|
||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
@@ -33,8 +33,6 @@ import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||
|
||||
function AppRoutes(props) {
|
||||
const {
|
||||
@@ -123,20 +121,6 @@ function AppRoutes(props) {
|
||||
component={BlocklistConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Wanted
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/wanted/missing"
|
||||
component={MissingConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/wanted/cutoffunmet"
|
||||
component={CutoffUnmetConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Settings
|
||||
*/}
|
||||
@@ -164,7 +148,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/settings/customformats"
|
||||
component={CustomFormatSettingsPage}
|
||||
component={CustomFormatSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.version {
|
||||
margin: 0 3px;
|
||||
font-weight: bold;
|
||||
font-family: var(--defaultFontFamily);
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
@@ -65,20 +64,20 @@ function AppUpdatedModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AppUpdated')}
|
||||
{translate('RadarrUpdated')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: translate('VersionUpdateText', [`<span className=${styles.version}>${version}</span>`]) }} />
|
||||
|
||||
{
|
||||
isPopulated && !error && !!update &&
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
49
frontend/src/App/ApplyTheme.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Radarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme() {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
||||
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('ConnectionLostToBackend')}
|
||||
{translate('ConnectionLostMessage')}
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
{translate('ConnectionLostReconnect')}
|
||||
{translate('ConnectionLostAutomaticMessage')}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
@@ -18,14 +17,7 @@ export interface AppSectionSaveState {
|
||||
}
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalRecords?: number;
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
@@ -41,7 +33,6 @@ export interface AppSectionItemState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
pendingChanges: Partial<T>;
|
||||
item: T;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
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 {
|
||||
@@ -45,31 +37,14 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
blocklist: BlocklistAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieIndex: MovieIndexAppState;
|
||||
movies: MoviesAppState;
|
||||
parse: ParseAppState;
|
||||
queue: QueueAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
movies: MoviesAppState;
|
||||
tags: TagsAppState;
|
||||
queue: QueueAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
|
||||
|
||||
interface BlocklistAppState
|
||||
extends AppSectionState<Blocklist>,
|
||||
AppSectionFilterState<Blocklist> {}
|
||||
|
||||
export default BlocklistAppState;
|
||||
@@ -1,10 +0,0 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Movie from 'Movie/Movie';
|
||||
|
||||
interface CalendarAppState
|
||||
extends AppSectionState<Movie>,
|
||||
AppSectionFilterState<Movie> {}
|
||||
|
||||
export default CalendarAppState;
|
||||
@@ -1,6 +0,0 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
||||
@@ -1,10 +0,0 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History> {}
|
||||
|
||||
export default HistoryAppState;
|
||||
@@ -1,7 +1,7 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
|
||||
import ImportMode from 'InteractiveImport/ImportMode';
|
||||
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
||||
import ImportMode from '../../InteractiveImport/ImportMode';
|
||||
import InteractiveImport from '../../InteractiveImport/InteractiveImport';
|
||||
|
||||
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
||||
originalItems: InteractiveImport[];
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import MovieCollection from 'typings/MovieCollection';
|
||||
|
||||
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
||||
itemMap: Record<number, number>;
|
||||
}
|
||||
|
||||
export default MovieCollectionAppState;
|
||||
@@ -22,10 +22,6 @@ export interface MovieIndexAppState {
|
||||
showQualityProfile: boolean;
|
||||
showReleaseDate: boolean;
|
||||
showCinemaRelease: boolean;
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
@@ -38,7 +34,6 @@ export interface MovieIndexAppState {
|
||||
showAdded: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import Movie from 'Movie/Movie';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
export interface ParsedMovieInfo {
|
||||
releaseTitle: string;
|
||||
originalTitle: string;
|
||||
movieTitle: string;
|
||||
movieTitles: string[];
|
||||
year: number;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
releaseHash: string;
|
||||
releaseGroup?: string;
|
||||
edition?: string;
|
||||
tmdbId?: number;
|
||||
imdbId?: string;
|
||||
}
|
||||
|
||||
export interface ParseModel extends ModelBase {
|
||||
title: string;
|
||||
parsedMovieInfo: ParsedMovieInfo;
|
||||
movie?: Movie;
|
||||
languages?: Language[];
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore?: number;
|
||||
}
|
||||
|
||||
type ParseAppState = AppSectionItemState<ParseModel>;
|
||||
|
||||
export default ParseAppState;
|
||||
@@ -1,17 +1,41 @@
|
||||
import Queue from 'typings/Queue';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
AppSectionItemState,
|
||||
Error,
|
||||
} from './AppSectionState';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
||||
|
||||
export interface StatusMessage {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
export interface Queue extends ModelBase {
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
statusMessages: StatusMessage[];
|
||||
errorMessage: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
outputPath: string;
|
||||
movieHasFile: boolean;
|
||||
movieId?: number;
|
||||
}
|
||||
|
||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface QueuePagedAppState
|
||||
extends AppSectionState<Queue>,
|
||||
AppSectionFilterState<Queue> {
|
||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
||||
isGrabbing: boolean;
|
||||
grabError: Error;
|
||||
isRemoving: boolean;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import RootFolder from 'typings/RootFolder';
|
||||
|
||||
interface RootFolderAppState
|
||||
extends AppSectionState<RootFolder>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export default RootFolderAppState;
|
||||
@@ -1,17 +1,12 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
PagedAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
@@ -39,34 +34,17 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface ImportListOptionsSettingsAppState
|
||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListExclusionsSettingsAppState
|
||||
extends AppSectionState<ImportListExclusion>,
|
||||
AppSectionSaveState,
|
||||
PagedAppSectionState,
|
||||
AppSectionDeleteState {
|
||||
pendingChanges: Partial<ImportListExclusion>;
|
||||
}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
downloadClients: DownloadClientAppState;
|
||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
languages: LanguageSettingsAppState;
|
||||
notifications: NotificationAppState;
|
||||
language: LanguageSettingsAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
|
||||
interface SystemAppState {
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
@@ -1,32 +1,12 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
|
||||
export default TagsAppState;
|
||||
|
||||
@@ -42,9 +42,9 @@ function Agenda(props) {
|
||||
<div className={styles.agenda}>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
const momentDate = moment(item.sortDate);
|
||||
const momentDate = moment(item.inCinemas);
|
||||
const showDate = index === 0 ||
|
||||
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
|
||||
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
|
||||
|
||||
return (
|
||||
<AgendaEventConnector
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
.event {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
}
|
||||
|
||||
.underlay {
|
||||
@add-mixin cover;
|
||||
font-size: $defaultFontSize;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tableRowHoverBackgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@add-mixin linkOverlay;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
font-size: $defaultFontSize;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
border-left-width: 5px;
|
||||
}
|
||||
.link {
|
||||
composes: link from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.eventWrapper {
|
||||
@@ -55,8 +44,6 @@
|
||||
|
||||
.statusIcon {
|
||||
margin-left: 3px;
|
||||
cursor: default;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -88,7 +75,7 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.overlay {
|
||||
.event {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -108,7 +95,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.releaseIcon {
|
||||
margin-right: 20px;
|
||||
.dateIcon {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
@@ -3,19 +3,18 @@
|
||||
interface CssExports {
|
||||
'continuing': string;
|
||||
'date': string;
|
||||
'dateIcon': string;
|
||||
'downloaded': string;
|
||||
'event': string;
|
||||
'eventWrapper': string;
|
||||
'genres': string;
|
||||
'link': string;
|
||||
'missingMonitored': string;
|
||||
'missingUnmonitored': string;
|
||||
'movieTitle': string;
|
||||
'overlay': string;
|
||||
'queue': string;
|
||||
'releaseIcon': string;
|
||||
'statusIcon': string;
|
||||
'time': string;
|
||||
'underlay': string;
|
||||
'unmonitored': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||