mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 200be6451a | |||
| b279984bd7 | |||
| 3f6f4fc65f |
@@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -118,7 +118,6 @@ src/UI/.idea/*
|
|||||||
node_modules/
|
node_modules/
|
||||||
_output*
|
_output*
|
||||||
_artifacts
|
_artifacts
|
||||||
_temp*
|
|
||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
@@ -127,7 +126,6 @@ coverage*.xml
|
|||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
.mono
|
|
||||||
|
|
||||||
# VS outout folders
|
# VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"ms-dotnettools.csdevkit",
|
|
||||||
"ms-vscode-remote.remote-containers"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Vendored
-26
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Vendored
-44
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Radarr
|
# Radarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||||
[](https://wiki.servarr.com/radarr/installation/docker)
|
[](https://wiki.servarr.com/radarr/installation#docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
[](#sponsors)
|
[](#sponsors)
|
||||||
|
|||||||
+20
-16
@@ -9,18 +9,18 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.10.3'
|
majorVersion: '5.1.2'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.424'
|
dotnetVersion: '6.0.413'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '16.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-12'
|
macImage: 'macOS-11'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -166,10 +166,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: UseNode@1
|
- task: NodeTool@0
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
version: $(nodeVersion)
|
versionSpec: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1089,10 +1089,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: UseNode@1
|
- task: NodeTool@0
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
version: $(nodeVersion)
|
versionSpec: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1116,7 +1116,7 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@1
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
@@ -1128,7 +1128,7 @@ stages:
|
|||||||
cliProjectName: 'RadarrUI'
|
cliProjectName: 'RadarrUI'
|
||||||
cliProjectVersion: '$(radarrVersion)'
|
cliProjectVersion: '$(radarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@1
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
@@ -1205,7 +1205,7 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@1
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
@@ -1223,21 +1223,25 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@1
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5
|
- task: reportgenerator@4
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
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
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- Analyze
|
- Analyze
|
||||||
- Installer
|
|
||||||
- Unit_Test
|
- Unit_Test
|
||||||
- Integration
|
- Integration
|
||||||
- Automation
|
- Automation
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ InstallInno()
|
|||||||
ProgressStart "Installing portable Inno Setup"
|
ProgressStart "Installing portable Inno Setup"
|
||||||
|
|
||||||
rm -rf _inno
|
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
|
mkdir _inno
|
||||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||||
rm innosetup.exe
|
rm innosetup.exe
|
||||||
|
|||||||
@@ -21,21 +21,15 @@ slnFile=src/Radarr.sln
|
|||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
|
||||||
application=Radarr.Console.dll
|
|
||||||
else
|
|
||||||
application=Radarr.dll
|
|
||||||
fi
|
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
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
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
+5
-44
@@ -359,16 +359,11 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
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',
|
'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',
|
'prettier/prettier': 'error',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'error',
|
'error',
|
||||||
@@ -381,41 +376,7 @@ module.exports = {
|
|||||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
['^@?\\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'
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ const loose = true;
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
'@babel/plugin-transform-logical-assignment-operators',
|
|
||||||
|
|
||||||
// Stage 1
|
// Stage 1
|
||||||
'@babel/plugin-proposal-export-default-from',
|
'@babel/plugin-proposal-export-default-from',
|
||||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ module.exports = (env) => {
|
|||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
filename: '[name]-[contenthash].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ module.exports = (env) => {
|
|||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
@@ -201,7 +201,7 @@ module.exports = (env) => {
|
|||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const mixinsFiles = [
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
'autoprefixer',
|
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
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 getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
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 BlocklistRowConnector from './BlocklistRowConnector';
|
||||||
|
|
||||||
|
class Blocklist extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isConfirmRemoveModalOpen: false,
|
||||||
|
items: props.items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (hasDifferentItems(prevProps.items, items)) {
|
||||||
|
this.setState((state) => {
|
||||||
|
return {
|
||||||
|
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||||
|
items
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveSelectedPress = () => {
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveSelectedConfirmed = () => {
|
||||||
|
this.props.onRemoveSelected(this.getSelectedIds());
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmRemoveModalClose = () => {
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
totalRecords,
|
||||||
|
isRemoving,
|
||||||
|
isClearingBlocklistExecuting,
|
||||||
|
onClearBlocklistPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmRemoveModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const selectedIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Blocklist')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RemoveSelected')}
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={!selectedIds.length}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={this.onRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Clear')}
|
||||||
|
iconName={icons.CLEAR}
|
||||||
|
isSpinning={isClearingBlocklistExecuting}
|
||||||
|
onPress={onClearBlocklistPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('BlocklistLoadError')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !items.length &&
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{translate('NoHistoryBlocklist')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
columns={columns}
|
||||||
|
{...otherProps}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<BlocklistRowConnector
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id] || false}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('RemoveSelected')}
|
||||||
|
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||||
|
confirmLabel={translate('RemoveSelected')}
|
||||||
|
onConfirm={this.onRemoveSelectedConfirmed}
|
||||||
|
onCancel={this.onConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Blocklist.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
|
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||||
|
onRemoveSelected: PropTypes.func.isRequired,
|
||||||
|
onClearBlocklistPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Blocklist;
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { SelectProvider } from 'App/SelectContext';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
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';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
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 usePaging from 'Components/Table/usePaging';
|
|
||||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import {
|
|
||||||
clearBlocklist,
|
|
||||||
fetchBlocklist,
|
|
||||||
gotoBlocklistPage,
|
|
||||||
removeBlocklistItems,
|
|
||||||
setBlocklistFilter,
|
|
||||||
setBlocklistSort,
|
|
||||||
setBlocklistTableOption,
|
|
||||||
} from 'Store/Actions/blocklistActions';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
|
||||||
import { TableOptionsChangePayload } from 'typings/Table';
|
|
||||||
import {
|
|
||||||
registerPagePopulator,
|
|
||||||
unregisterPagePopulator,
|
|
||||||
} from 'Utilities/pagePopulator';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
|
||||||
import BlocklistRow from './BlocklistRow';
|
|
||||||
|
|
||||||
function Blocklist() {
|
|
||||||
const requestCurrentPage = useCurrentPage();
|
|
||||||
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
totalPages,
|
|
||||||
totalRecords,
|
|
||||||
isRemoving,
|
|
||||||
} = useSelector((state: AppState) => state.blocklist);
|
|
||||||
|
|
||||||
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
|
|
||||||
const isClearingBlocklistExecuting = useSelector(
|
|
||||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
|
|
||||||
);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const [selectState, setSelectState] = useSelectState();
|
|
||||||
const { allSelected, allUnselected, selectedState } = selectState;
|
|
||||||
|
|
||||||
const selectedIds = useMemo(() => {
|
|
||||||
return getSelectedIds(selectedState);
|
|
||||||
}, [selectedState]);
|
|
||||||
|
|
||||||
const wasClearingBlocklistExecuting = usePrevious(
|
|
||||||
isClearingBlocklistExecuting
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectAllChange = useCallback(
|
|
||||||
({ value }: CheckInputChanged) => {
|
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
|
||||||
},
|
|
||||||
[items, setSelectState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectedChange = useCallback(
|
|
||||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
|
||||||
setSelectState({
|
|
||||||
type: 'toggleSelected',
|
|
||||||
items,
|
|
||||||
id,
|
|
||||||
isSelected: value,
|
|
||||||
shiftKey,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[items, setSelectState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemoveSelectedPress = useCallback(() => {
|
|
||||||
setIsConfirmRemoveModalOpen(true);
|
|
||||||
}, [setIsConfirmRemoveModalOpen]);
|
|
||||||
|
|
||||||
const handleRemoveSelectedConfirmed = useCallback(() => {
|
|
||||||
dispatch(removeBlocklistItems({ ids: selectedIds }));
|
|
||||||
setIsConfirmRemoveModalOpen(false);
|
|
||||||
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
|
|
||||||
|
|
||||||
const handleConfirmRemoveModalClose = useCallback(() => {
|
|
||||||
setIsConfirmRemoveModalOpen(false);
|
|
||||||
}, [setIsConfirmRemoveModalOpen]);
|
|
||||||
|
|
||||||
const handleClearBlocklistPress = useCallback(() => {
|
|
||||||
setIsConfirmClearModalOpen(true);
|
|
||||||
}, [setIsConfirmClearModalOpen]);
|
|
||||||
|
|
||||||
const handleClearBlocklistConfirmed = useCallback(() => {
|
|
||||||
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
|
|
||||||
setIsConfirmClearModalOpen(false);
|
|
||||||
}, [setIsConfirmClearModalOpen, dispatch]);
|
|
||||||
|
|
||||||
const handleConfirmClearModalClose = useCallback(() => {
|
|
||||||
setIsConfirmClearModalOpen(false);
|
|
||||||
}, [setIsConfirmClearModalOpen]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleFirstPagePress,
|
|
||||||
handlePreviousPagePress,
|
|
||||||
handleNextPagePress,
|
|
||||||
handleLastPagePress,
|
|
||||||
handlePageSelect,
|
|
||||||
} = usePaging({
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
gotoPage: gotoBlocklistPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
|
||||||
(selectedFilterKey: string) => {
|
|
||||||
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSortPress = useCallback(
|
|
||||||
(sortKey: string) => {
|
|
||||||
dispatch(setBlocklistSort({ sortKey }));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTableOptionChange = useCallback(
|
|
||||||
(payload: TableOptionsChangePayload) => {
|
|
||||||
dispatch(setBlocklistTableOption(payload));
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (requestCurrentPage) {
|
|
||||||
dispatch(fetchBlocklist());
|
|
||||||
} else {
|
|
||||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
dispatch(clearBlocklist());
|
|
||||||
};
|
|
||||||
}, [requestCurrentPage, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const repopulate = () => {
|
|
||||||
dispatch(fetchBlocklist());
|
|
||||||
};
|
|
||||||
|
|
||||||
registerPagePopulator(repopulate);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unregisterPagePopulator(repopulate);
|
|
||||||
};
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
|
|
||||||
dispatch(gotoBlocklistPage({ page: 1 }));
|
|
||||||
}
|
|
||||||
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectProvider items={items}>
|
|
||||||
<PageContent title={translate('Blocklist')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RemoveSelected')}
|
|
||||||
iconName={icons.REMOVE}
|
|
||||||
isDisabled={!selectedIds.length}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={handleRemoveSelectedPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Clear')}
|
|
||||||
iconName={icons.CLEAR}
|
|
||||||
isDisabled={!items.length}
|
|
||||||
isSpinning={isClearingBlocklistExecuting}
|
|
||||||
onPress={handleClearBlocklistPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
columns={columns}
|
|
||||||
pageSize={pageSize}
|
|
||||||
onTableOptionChange={handleTableOptionChange}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={BlocklistFilterModal}
|
|
||||||
onFilterSelect={handleFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
|
||||||
|
|
||||||
{!isFetching && !!error ? (
|
|
||||||
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isPopulated && !error && !items.length ? (
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{selectedFilterKey === 'all'
|
|
||||||
? translate('NoBlocklistItems')
|
|
||||||
: translate('BlocklistFilterHasNoItems')}
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isPopulated && !error && !!items.length ? (
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
columns={columns}
|
|
||||||
pageSize={pageSize}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onTableOptionChange={handleTableOptionChange}
|
|
||||||
onSelectAllChange={handleSelectAllChange}
|
|
||||||
onSortPress={handleSortPress}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{items.map((item) => {
|
|
||||||
return (
|
|
||||||
<BlocklistRow
|
|
||||||
key={item.id}
|
|
||||||
isSelected={selectedState[item.id] || false}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
onSelectedChange={handleSelectedChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<TablePager
|
|
||||||
page={page}
|
|
||||||
totalPages={totalPages}
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetching}
|
|
||||||
onFirstPagePress={handleFirstPagePress}
|
|
||||||
onPreviousPagePress={handlePreviousPagePress}
|
|
||||||
onNextPagePress={handleNextPagePress}
|
|
||||||
onLastPagePress={handleLastPagePress}
|
|
||||||
onPageSelect={handlePageSelect}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('RemoveSelected')}
|
|
||||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
|
||||||
confirmLabel={translate('RemoveSelected')}
|
|
||||||
onConfirm={handleRemoveSelectedConfirmed}
|
|
||||||
onCancel={handleConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isConfirmClearModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('ClearBlocklist')}
|
|
||||||
message={translate('ClearBlocklistMessageText')}
|
|
||||||
confirmLabel={translate('Clear')}
|
|
||||||
onConfirm={handleClearBlocklistConfirmed}
|
|
||||||
onCancel={handleConfirmClearModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
</SelectProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Blocklist;
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
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 createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import Blocklist from './Blocklist';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.blocklist,
|
||||||
|
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||||
|
(blocklist, isClearingBlocklistExecuting) => {
|
||||||
|
return {
|
||||||
|
isClearingBlocklistExecuting,
|
||||||
|
...blocklist
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...blocklistActions,
|
||||||
|
executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlocklistConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchBlocklist,
|
||||||
|
gotoBlocklistFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchBlocklist();
|
||||||
|
} else {
|
||||||
|
gotoBlocklistFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
||||||
|
this.props.gotoBlocklistFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.clearBlocklist();
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchBlocklist();
|
||||||
|
};
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoBlocklistFirstPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoBlocklistPreviousPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoBlocklistNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoBlocklistLastPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoBlocklistPage({ page });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveSelected = (ids) => {
|
||||||
|
this.props.removeBlocklistItems({ ids });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setBlocklistSort({ sortKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setBlocklistTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoBlocklistFirstPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onClearBlocklistPress = () => {
|
||||||
|
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Blocklist
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onRemoveSelected={this.onRemoveSelected}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocklistConnector.propTypes = {
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
|
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchBlocklist: PropTypes.func.isRequired,
|
||||||
|
gotoBlocklistFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlocklistNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlocklistLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||||
|
removeBlocklistItems: PropTypes.func.isRequired,
|
||||||
|
setBlocklistSort: PropTypes.func.isRequired,
|
||||||
|
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||||
|
clearBlocklist: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
||||||
|
);
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
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 translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
class BlocklistDetailsModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
sourceTitle,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
message,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
Details
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Protocol')}
|
||||||
|
data={protocol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!message &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Indexer')}
|
||||||
|
data={indexer}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!message &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Message')}
|
||||||
|
data={message}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</DescriptionList>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
{translate('Close')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocklistDetailsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
indexer: PropTypes.string,
|
||||||
|
message: PropTypes.string,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlocklistDetailsModal;
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
interface BlocklistDetailsModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
sourceTitle: string;
|
|
||||||
protocol: DownloadProtocol;
|
|
||||||
indexer?: string;
|
|
||||||
message?: string;
|
|
||||||
onModalClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
|
|
||||||
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
|
|
||||||
props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>Details</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Protocol')}
|
|
||||||
data={protocol}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{message ? (
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{message ? (
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DescriptionList>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BlocklistDetailsModal;
|
|
||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.languages,
|
.language,
|
||||||
.quality {
|
.quality {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'indexer': string;
|
'indexer': string;
|
||||||
'languages': string;
|
'language': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
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 { icons, kinds } from 'Helpers/Props';
|
||||||
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||||
|
import styles from './BlocklistRow.css';
|
||||||
|
|
||||||
|
class BlocklistRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isDetailsModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDetailsPress = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onDetailsModalClose = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movie,
|
||||||
|
sourceTitle,
|
||||||
|
quality,
|
||||||
|
customFormats,
|
||||||
|
languages,
|
||||||
|
date,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
message,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
onRemovePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink
|
||||||
|
titleSlug={movie.titleSlug}
|
||||||
|
title={movie.title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{sourceTitle}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieLanguage
|
||||||
|
languages={languages}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.quality}
|
||||||
|
>
|
||||||
|
<MovieQuality
|
||||||
|
quality={quality}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieFormats
|
||||||
|
formats={customFormats}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={date}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexer}
|
||||||
|
>
|
||||||
|
{indexer}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.actions}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
name={icons.INFO}
|
||||||
|
onPress={this.onDetailsPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={translate('RemoveFromBlocklist')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onRemovePress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<BlocklistDetailsModal
|
||||||
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
protocol={protocol}
|
||||||
|
indexer={indexer}
|
||||||
|
message={message}
|
||||||
|
onModalClose={this.onDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocklistRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
movie: PropTypes.object.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
quality: PropTypes.object.isRequired,
|
||||||
|
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
indexer: PropTypes.string,
|
||||||
|
message: PropTypes.string,
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlocklistRow;
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
|
||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
|
||||||
import useMovie from 'Movie/useMovie';
|
|
||||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
|
||||||
import Blocklist from 'typings/Blocklist';
|
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
|
||||||
import styles from './BlocklistRow.css';
|
|
||||||
|
|
||||||
interface BlocklistRowProps extends Blocklist {
|
|
||||||
isSelected: boolean;
|
|
||||||
columns: Column[];
|
|
||||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function BlocklistRow(props: BlocklistRowProps) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
movieId,
|
|
||||||
sourceTitle,
|
|
||||||
languages,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
date,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
message,
|
|
||||||
isSelected,
|
|
||||||
columns,
|
|
||||||
onSelectedChange,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const movie = useMovie(movieId);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleDetailsPress = useCallback(() => {
|
|
||||||
setIsDetailsModalOpen(true);
|
|
||||||
}, [setIsDetailsModalOpen]);
|
|
||||||
|
|
||||||
const handleDetailsModalClose = useCallback(() => {
|
|
||||||
setIsDetailsModalOpen(false);
|
|
||||||
}, [setIsDetailsModalOpen]);
|
|
||||||
|
|
||||||
const handleRemovePress = useCallback(() => {
|
|
||||||
dispatch(removeBlocklistItem({ id }));
|
|
||||||
}, [id, dispatch]);
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{columns.map((column) => {
|
|
||||||
const { name, isVisible } = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'movieMetadata.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sourceTitle') {
|
|
||||||
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name} className={styles.languages}>
|
|
||||||
<MovieLanguages languages={languages} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name} className={styles.quality}>
|
|
||||||
<MovieQuality quality={quality} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieFormats formats={customFormats} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'date') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore ts(2739)
|
|
||||||
return <RelativeDateCell key={name} date={date} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name} className={styles.indexer}>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name} className={styles.actions}>
|
|
||||||
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
title={translate('RemoveFromBlocklist')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={handleRemovePress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
|
|
||||||
<BlocklistDetailsModal
|
|
||||||
isOpen={isDetailsModalOpen}
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
protocol={protocol}
|
|
||||||
indexer={indexer}
|
|
||||||
message={message}
|
|
||||||
onModalClose={handleDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BlocklistRow;
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
|
import BlocklistRow from './BlocklistRow';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createMovieSelector(),
|
||||||
|
(movie) => {
|
||||||
|
return {
|
||||||
|
movie
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onRemovePress() {
|
||||||
|
dispatch(removeBlocklistItem({ id: props.id }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
||||||
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType, data) {
|
function getIconName(eventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@@ -17,7 +17,7 @@ function getIconName(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
return icons.DELETE;
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('MovieDownloadFailedTooltip');
|
return translate('MovieDownloadFailedTooltip');
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
|
return translate('MovieFileDeletedTooltip');
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return translate('MovieFileRenamedTooltip');
|
return translate('MovieFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
function HistoryEventTypeCell({ eventType, data }) {
|
||||||
const iconName = getIconName(eventType, data);
|
const iconName = getIconName(eventType);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
@@ -113,7 +113,7 @@ class HistoryRow extends Component {
|
|||||||
if (name === 'languages') {
|
if (name === 'languages') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<MovieLanguages
|
<MovieLanguage
|
||||||
languages={languages}
|
languages={languages}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
@@ -143,7 +143,7 @@ class HistoryRow extends Component {
|
|||||||
|
|
||||||
if (name === 'date') {
|
if (name === 'date') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCell
|
<RelativeDateCellConnector
|
||||||
key={name}
|
key={name}
|
||||||
date={date}
|
date={date}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,7 +11,3 @@
|
|||||||
border-color: var(--usenetColor);
|
border-color: var(--usenetColor);
|
||||||
background-color: var(--usenetColor);
|
background-color: var(--usenetColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.unknown {
|
|
||||||
composes: label from '~Components/Label.css';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'torrent': string;
|
'torrent': string;
|
||||||
'unknown': string;
|
|
||||||
'usenet': string;
|
'usenet': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import styles from './ProtocolLabel.css';
|
||||||
|
|
||||||
|
function ProtocolLabel({ protocol }) {
|
||||||
|
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label className={styles[protocol]}>
|
||||||
|
{protocolName}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtocolLabel.propTypes = {
|
||||||
|
protocol: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtocolLabel;
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|
||||||
import styles from './ProtocolLabel.css';
|
|
||||||
|
|
||||||
interface ProtocolLabelProps {
|
|
||||||
protocol: DownloadProtocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
|
|
||||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
|
||||||
|
|
||||||
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProtocolLabel;
|
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
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';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
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 getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
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 RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||||
|
|
||||||
|
class Queue extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this._shouldBlockRefresh = false;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isPendingSelected: false,
|
||||||
|
isConfirmRemoveModalOpen: false,
|
||||||
|
items: props.items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate() {
|
||||||
|
if (this._shouldBlockRefresh) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
isFetching,
|
||||||
|
isMoviesFetching
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!isMoviesFetching && prevProps.isMoviesFetching) ||
|
||||||
|
(!isFetching && prevProps.isFetching) ||
|
||||||
|
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
|
||||||
|
) {
|
||||||
|
this.setState((state) => {
|
||||||
|
return {
|
||||||
|
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||||
|
items
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextState = {};
|
||||||
|
|
||||||
|
if (prevProps.items !== items) {
|
||||||
|
nextState.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds = this.getSelectedIds();
|
||||||
|
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||||
|
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||||
|
nextState.isPendingSelected = isPendingSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(nextState)) {
|
||||||
|
this.setState(nextState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||||
|
this._shouldBlockRefresh = isOpen;
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onGrabSelectedPress = () => {
|
||||||
|
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveSelectedPress = () => {
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
||||||
|
this._shouldBlockRefresh = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveSelectedConfirmed = (payload) => {
|
||||||
|
this._shouldBlockRefresh = false;
|
||||||
|
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmRemoveModalClose = () => {
|
||||||
|
this._shouldBlockRefresh = false;
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isMoviesFetching,
|
||||||
|
isMoviesPopulated,
|
||||||
|
moviesError,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
count,
|
||||||
|
totalRecords,
|
||||||
|
isGrabbing,
|
||||||
|
isRemoving,
|
||||||
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
|
onRefreshPress,
|
||||||
|
onFilterSelect,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmRemoveModalOpen,
|
||||||
|
isPendingSelected,
|
||||||
|
items
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||||
|
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
|
||||||
|
const hasError = error || moviesError;
|
||||||
|
const selectedIds = this.getSelectedIds();
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
const disableSelectedActions = selectedCount === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Queue')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Refresh')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshing}
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('GrabSelected')}
|
||||||
|
iconName={icons.DOWNLOAD}
|
||||||
|
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={this.onGrabSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RemoveSelected')}
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={disableSelectedActions}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={this.onRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection
|
||||||
|
alignContent={align.RIGHT}
|
||||||
|
>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
{...otherProps}
|
||||||
|
optionsComponent={QueueOptionsConnector}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={QueueFilterModal}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{
|
||||||
|
isRefreshing && !isAllPopulated ?
|
||||||
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isRefreshing && hasError ?
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('QueueLoadError')}
|
||||||
|
</Alert> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isAllPopulated && !hasError && !items.length ?
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{
|
||||||
|
selectedFilterKey !== 'all' && count > 0 ?
|
||||||
|
translate('QueueFilterHasNoItems') :
|
||||||
|
translate('QueueIsEmpty')
|
||||||
|
}
|
||||||
|
</Alert> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isAllPopulated && !hasError && !!items.length ?
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
{...otherProps}
|
||||||
|
optionsComponent={QueueOptionsConnector}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<QueueRowConnector
|
||||||
|
key={item.id}
|
||||||
|
movieId={item.movieId}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isRefreshing}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
<RemoveQueueItemsModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
selectedCount={selectedCount}
|
||||||
|
canIgnore={isConfirmRemoveModalOpen && (
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.movieId);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
allPending={isConfirmRemoveModalOpen && (
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||||
|
onModalClose={this.onConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isMoviesFetching: PropTypes.bool.isRequired,
|
||||||
|
isMoviesPopulated: PropTypes.bool.isRequired,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Queue;
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
import React, {
|
|
||||||
ReactElement,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
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';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
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 usePaging from 'Components/Table/usePaging';
|
|
||||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import {
|
|
||||||
clearQueue,
|
|
||||||
fetchQueue,
|
|
||||||
gotoQueuePage,
|
|
||||||
grabQueueItems,
|
|
||||||
removeQueueItems,
|
|
||||||
setQueueFilter,
|
|
||||||
setQueueSort,
|
|
||||||
setQueueTableOption,
|
|
||||||
} from 'Store/Actions/queueActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
|
||||||
import { TableOptionsChangePayload } from 'typings/Table';
|
|
||||||
import {
|
|
||||||
registerPagePopulator,
|
|
||||||
unregisterPagePopulator,
|
|
||||||
} from 'Utilities/pagePopulator';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import QueueFilterModal from './QueueFilterModal';
|
|
||||||
import QueueOptions from './QueueOptions';
|
|
||||||
import QueueRow from './QueueRow';
|
|
||||||
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
|
||||||
import createQueueStatusSelector from './Status/createQueueStatusSelector';
|
|
||||||
|
|
||||||
function Queue() {
|
|
||||||
const requestCurrentPage = useCurrentPage();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
totalPages,
|
|
||||||
totalRecords,
|
|
||||||
isGrabbing,
|
|
||||||
isRemoving,
|
|
||||||
} = useSelector((state: AppState) => state.queue.paged);
|
|
||||||
|
|
||||||
const { count } = useSelector(createQueueStatusSelector());
|
|
||||||
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
|
|
||||||
createMoviesFetchingSelector()
|
|
||||||
);
|
|
||||||
const customFilters = useSelector(createCustomFiltersSelector('queue'));
|
|
||||||
|
|
||||||
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldBlockRefresh = useRef(false);
|
|
||||||
const currentQueue = useRef<ReactElement | null>(null);
|
|
||||||
|
|
||||||
const [selectState, setSelectState] = useSelectState();
|
|
||||||
const { allSelected, allUnselected, selectedState } = selectState;
|
|
||||||
|
|
||||||
const selectedIds = useMemo(() => {
|
|
||||||
return getSelectedIds(selectedState);
|
|
||||||
}, [selectedState]);
|
|
||||||
|
|
||||||
const isPendingSelected = useMemo(() => {
|
|
||||||
return items.some((item) => {
|
|
||||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
|
||||||
});
|
|
||||||
}, [items, selectedIds]);
|
|
||||||
|
|
||||||
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const isRefreshing =
|
|
||||||
isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
|
||||||
const isAllPopulated =
|
|
||||||
isPopulated &&
|
|
||||||
(isMoviesPopulated || !items.length || items.every((m) => !m.movieId));
|
|
||||||
const hasError = error || moviesError;
|
|
||||||
const selectedCount = selectedIds.length;
|
|
||||||
const disableSelectedActions = selectedCount === 0;
|
|
||||||
|
|
||||||
const handleSelectAllChange = useCallback(
|
|
||||||
({ value }: CheckInputChanged) => {
|
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
|
||||||
},
|
|
||||||
[items, setSelectState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectedChange = useCallback(
|
|
||||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
|
||||||
setSelectState({
|
|
||||||
type: 'toggleSelected',
|
|
||||||
items,
|
|
||||||
id,
|
|
||||||
isSelected: value,
|
|
||||||
shiftKey,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[items, setSelectState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRefreshPress = useCallback(() => {
|
|
||||||
dispatch(
|
|
||||||
executeCommand({
|
|
||||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
|
|
||||||
shouldBlockRefresh.current = isOpen;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleGrabSelectedPress = useCallback(() => {
|
|
||||||
dispatch(grabQueueItems({ ids: selectedIds }));
|
|
||||||
}, [selectedIds, dispatch]);
|
|
||||||
|
|
||||||
const handleRemoveSelectedPress = useCallback(() => {
|
|
||||||
shouldBlockRefresh.current = true;
|
|
||||||
setIsConfirmRemoveModalOpen(true);
|
|
||||||
}, [setIsConfirmRemoveModalOpen]);
|
|
||||||
|
|
||||||
const handleRemoveSelectedConfirmed = useCallback(
|
|
||||||
(payload: RemovePressProps) => {
|
|
||||||
shouldBlockRefresh.current = false;
|
|
||||||
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
|
|
||||||
setIsConfirmRemoveModalOpen(false);
|
|
||||||
},
|
|
||||||
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleConfirmRemoveModalClose = useCallback(() => {
|
|
||||||
shouldBlockRefresh.current = false;
|
|
||||||
setIsConfirmRemoveModalOpen(false);
|
|
||||||
}, [setIsConfirmRemoveModalOpen]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleFirstPagePress,
|
|
||||||
handlePreviousPagePress,
|
|
||||||
handleNextPagePress,
|
|
||||||
handleLastPagePress,
|
|
||||||
handlePageSelect,
|
|
||||||
} = usePaging({
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
gotoPage: gotoQueuePage,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
|
||||||
(selectedFilterKey: string) => {
|
|
||||||
dispatch(setQueueFilter({ selectedFilterKey }));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSortPress = useCallback(
|
|
||||||
(sortKey: string) => {
|
|
||||||
dispatch(setQueueSort({ sortKey }));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTableOptionChange = useCallback(
|
|
||||||
(payload: TableOptionsChangePayload) => {
|
|
||||||
dispatch(setQueueTableOption(payload));
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
dispatch(gotoQueuePage({ page: 1 }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (requestCurrentPage) {
|
|
||||||
dispatch(fetchQueue());
|
|
||||||
} else {
|
|
||||||
dispatch(gotoQueuePage({ page: 1 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
dispatch(clearQueue());
|
|
||||||
};
|
|
||||||
}, [requestCurrentPage, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const repopulate = () => {
|
|
||||||
dispatch(fetchQueue());
|
|
||||||
};
|
|
||||||
|
|
||||||
registerPagePopulator(repopulate);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unregisterPagePopulator(repopulate);
|
|
||||||
};
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
if (!shouldBlockRefresh.current) {
|
|
||||||
currentQueue.current = (
|
|
||||||
<PageContentBody>
|
|
||||||
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
|
|
||||||
|
|
||||||
{!isRefreshing && hasError ? (
|
|
||||||
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isAllPopulated && !hasError && !items.length ? (
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{selectedFilterKey !== 'all' && count > 0
|
|
||||||
? translate('QueueFilterHasNoItems')
|
|
||||||
: translate('QueueIsEmpty')}
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isAllPopulated && !hasError && !!items.length ? (
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
columns={columns}
|
|
||||||
pageSize={pageSize}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
optionsComponent={QueueOptions}
|
|
||||||
onTableOptionChange={handleTableOptionChange}
|
|
||||||
onSelectAllChange={handleSelectAllChange}
|
|
||||||
onSortPress={handleSortPress}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QueueRow
|
|
||||||
key={item.id}
|
|
||||||
movieId={item.movieId}
|
|
||||||
isSelected={selectedState[item.id]}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
onSelectedChange={handleSelectedChange}
|
|
||||||
onQueueRowModalOpenOrClose={
|
|
||||||
handleQueueRowModalOpenOrClose
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
page={page}
|
|
||||||
totalPages={totalPages}
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetching}
|
|
||||||
onFirstPagePress={handleFirstPagePress}
|
|
||||||
onPreviousPagePress={handlePreviousPagePress}
|
|
||||||
onNextPagePress={handleNextPagePress}
|
|
||||||
onLastPagePress={handleLastPagePress}
|
|
||||||
onPageSelect={handlePageSelect}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</PageContentBody>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Queue')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label="Refresh"
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isRefreshing}
|
|
||||||
onPress={handleRefreshPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('GrabSelected')}
|
|
||||||
iconName={icons.DOWNLOAD}
|
|
||||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={handleGrabSelectedPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RemoveSelected')}
|
|
||||||
iconName={icons.REMOVE}
|
|
||||||
isDisabled={disableSelectedActions}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={handleRemoveSelectedPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
columns={columns}
|
|
||||||
pageSize={pageSize}
|
|
||||||
maxPageSize={200}
|
|
||||||
optionsComponent={QueueOptions}
|
|
||||||
onTableOptionChange={handleTableOptionChange}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={QueueFilterModal}
|
|
||||||
onFilterSelect={handleFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
{currentQueue.current}
|
|
||||||
|
|
||||||
<RemoveQueueItemModal
|
|
||||||
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);
|
|
||||||
|
|
||||||
return !!(item && item.movieId);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
isPending={
|
|
||||||
isConfirmRemoveModalOpen &&
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
item.status === 'delay' ||
|
|
||||||
item.status === 'downloadClientUnavailable'
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onRemovePress={handleRemoveSelectedConfirmed}
|
|
||||||
onModalClose={handleConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Queue;
|
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
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';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(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) => {
|
||||||
|
return {
|
||||||
|
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
|
||||||
|
isMoviesFetching: movies.isFetching,
|
||||||
|
isMoviesPopulated: movies.isPopulated,
|
||||||
|
moviesError: movies.error,
|
||||||
|
customFilters,
|
||||||
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
|
...options,
|
||||||
|
...queue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...queueActions,
|
||||||
|
executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class QueueConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchQueue,
|
||||||
|
fetchQueueStatus,
|
||||||
|
gotoQueueFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchQueue();
|
||||||
|
} else {
|
||||||
|
gotoQueueFirstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchQueueStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (
|
||||||
|
this.props.includeUnknownMovieItems !==
|
||||||
|
prevProps.includeUnknownMovieItems
|
||||||
|
) {
|
||||||
|
this.repopulate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
this.props.clearQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoQueueFirstPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoQueuePreviousPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoQueueNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoQueueLastPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoQueuePage({ page });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setQueueSort({ sortKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setQueueFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setQueueTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoQueueFirstPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onGrabSelectedPress = (ids) => {
|
||||||
|
this.props.grabQueueItems({ ids });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveSelectedPress = (payload) => {
|
||||||
|
this.props.removeQueueItems(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Queue
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onRefreshPress={this.onRefreshPress}
|
||||||
|
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||||
|
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueConnector.propTypes = {
|
||||||
|
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchQueue: PropTypes.func.isRequired,
|
||||||
|
fetchQueueStatus: PropTypes.func.isRequired,
|
||||||
|
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoQueueNextPage: PropTypes.func.isRequired,
|
||||||
|
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,
|
||||||
|
removeQueueItems: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
||||||
|
);
|
||||||
+28
-26
@@ -1,49 +1,36 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import {
|
|
||||||
QueueTrackedDownloadState,
|
|
||||||
QueueTrackedDownloadStatus,
|
|
||||||
StatusMessage,
|
|
||||||
} from 'typings/Queue';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueueStatus from './QueueStatus';
|
import QueueStatus from './QueueStatus';
|
||||||
import styles from './QueueDetails.css';
|
import styles from './QueueDetails.css';
|
||||||
|
|
||||||
interface QueueDetailsProps {
|
function QueueDetails(props) {
|
||||||
title: string;
|
|
||||||
size: number;
|
|
||||||
sizeleft: number;
|
|
||||||
estimatedCompletionTime?: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadState?: QueueTrackedDownloadState;
|
|
||||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
|
||||||
statusMessages?: StatusMessage[];
|
|
||||||
errorMessage?: string;
|
|
||||||
progressBar: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function QueueDetails(props: QueueDetailsProps) {
|
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
status,
|
status,
|
||||||
trackedDownloadState = 'downloading',
|
trackedDownloadState,
|
||||||
trackedDownloadStatus = 'ok',
|
trackedDownloadStatus,
|
||||||
statusMessages,
|
statusMessages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
progressBar,
|
progressBar
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||||
const isDownloading = status === 'downloading';
|
const isDownloading = status === 'downloading';
|
||||||
const isPaused = status === 'paused';
|
const isPaused = status === 'paused';
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
const hasError = trackedDownloadStatus === 'error';
|
const hasError = trackedDownloadStatus === 'error';
|
||||||
|
|
||||||
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
|
if (
|
||||||
|
(isDownloading || isPaused) &&
|
||||||
|
!hasWarning &&
|
||||||
|
!hasError
|
||||||
|
) {
|
||||||
const state = isPaused ? translate('Paused') : translate('Downloading');
|
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||||
|
|
||||||
if (progress < 5) {
|
if (progress < 5) {
|
||||||
@@ -58,9 +45,11 @@ function QueueDetails(props: QueueDetailsProps) {
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
className={styles.progressBarContainer}
|
className={styles.progressBarContainer}
|
||||||
anchor={progressBar!}
|
anchor={progressBar}
|
||||||
title={`${state} - ${progress.toFixed(1)}%`}
|
title={`${state} - ${progress.toFixed(1)}%`}
|
||||||
body={<div>{title}</div>}
|
body={
|
||||||
|
<div>{title}</div>
|
||||||
|
}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -79,4 +68,17 @@ function QueueDetails(props: QueueDetailsProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueueDetails.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
sizeleft: PropTypes.number.isRequired,
|
||||||
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadState: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||||
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
progressBar: PropTypes.node.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default QueueDetails;
|
export default QueueDetails;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
class QueueOptions extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
includeUnknownMovieItems: props.includeUnknownMovieItems
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
includeUnknownMovieItems
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
|
||||||
|
this.setState({
|
||||||
|
includeUnknownMovieItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOptionChange = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onOptionChange({
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
includeUnknownMovieItems
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeUnknownMovieItems"
|
||||||
|
value={includeUnknownMovieItems}
|
||||||
|
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||||
|
onChange={this.onOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOptions.propTypes = {
|
||||||
|
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||||
|
onOptionChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueOptions;
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function QueueOptions() {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { includeUnknownMovieItems } = useSelector(
|
|
||||||
(state: AppState) => state.queue.options
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOptionChange = useCallback(
|
|
||||||
({ name, value }: CheckInputChanged) => {
|
|
||||||
dispatch(
|
|
||||||
setQueueOption({
|
|
||||||
[name]: value,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (name === 'includeUnknownMovieItems') {
|
|
||||||
dispatch(gotoQueuePage({ page: 1 }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="includeUnknownMovieItems"
|
|
||||||
value={includeUnknownMovieItems}
|
|
||||||
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
|
||||||
onChange={handleOptionChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QueueOptions;
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||||
|
import QueueOptions from './QueueOptions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.queue.options,
|
||||||
|
(options) => {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
onOptionChange: setQueueOption
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
||||||
@@ -26,5 +26,4 @@
|
|||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 70px;
|
width: 70px;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
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 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 InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
|
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';
|
||||||
|
import TimeleftCell from './TimeleftCell';
|
||||||
|
import styles from './QueueRow.css';
|
||||||
|
|
||||||
|
class QueueRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isRemoveQueueItemModalOpen: false,
|
||||||
|
isInteractiveImportModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRemoveQueueItemPress = () => {
|
||||||
|
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
||||||
|
const {
|
||||||
|
onRemoveQueueItemPress,
|
||||||
|
onQueueRowModalOpenOrClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
onRemoveQueueItemPress(blocklist, skipRedownload);
|
||||||
|
|
||||||
|
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveQueueItemModalClose = () => {
|
||||||
|
this.props.onQueueRowModalOpenOrClose(false);
|
||||||
|
|
||||||
|
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveImportPress = () => {
|
||||||
|
this.props.onQueueRowModalOpenOrClose(true);
|
||||||
|
|
||||||
|
this.setState({ isInteractiveImportModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveImportModalClose = () => {
|
||||||
|
this.props.onQueueRowModalOpenOrClose(false);
|
||||||
|
|
||||||
|
this.setState({ isInteractiveImportModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
downloadId,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
movie,
|
||||||
|
quality,
|
||||||
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
|
languages,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
outputPath,
|
||||||
|
downloadClient,
|
||||||
|
estimatedCompletionTime,
|
||||||
|
timeleft,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
isGrabbing,
|
||||||
|
grabError,
|
||||||
|
isRemoving,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
onGrabPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isRemoveQueueItemModalOpen,
|
||||||
|
isInteractiveImportModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const progress = 100 - (sizeleft / size * 100);
|
||||||
|
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||||
|
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<QueueStatusCell
|
||||||
|
key={name}
|
||||||
|
sourceTitle={title}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movies.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
movie ?
|
||||||
|
<MovieTitleLink
|
||||||
|
titleSlug={movie.titleSlug}
|
||||||
|
title={movie.title}
|
||||||
|
/> :
|
||||||
|
title
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieLanguage
|
||||||
|
languages={languages}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
quality ?
|
||||||
|
<MovieQuality
|
||||||
|
quality={quality}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieFormats
|
||||||
|
formats={customFormats}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}>
|
||||||
|
<ProtocolLabel
|
||||||
|
protocol={protocol}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{indexer}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{downloadClient}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{formatBytes(size)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'year') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
movie ? movie.year : ''
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'title') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{title}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'outputPath') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{outputPath}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'estimatedCompletionTime') {
|
||||||
|
return (
|
||||||
|
<TimeleftCell
|
||||||
|
key={name}
|
||||||
|
status={status}
|
||||||
|
estimatedCompletionTime={estimatedCompletionTime}
|
||||||
|
timeleft={timeleft}
|
||||||
|
size={size}
|
||||||
|
sizeleft={sizeleft}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'progress') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.progress}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
!!progress &&
|
||||||
|
<ProgressBar
|
||||||
|
progress={progress}
|
||||||
|
title={`${progress.toFixed(1)}%`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.actions}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
showInteractiveImport &&
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={this.onInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPending &&
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<SpinnerIconButton
|
||||||
|
title={translate('RemoveFromQueue')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={this.onRemoveQueueItemPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
downloadId={downloadId}
|
||||||
|
title={title}
|
||||||
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
|
sourceTitle={title}
|
||||||
|
canIgnore={!!movie}
|
||||||
|
isPending={isPending}
|
||||||
|
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||||
|
onModalClose={this.onRemoveQueueItemModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
downloadId: PropTypes.string,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadStatus: PropTypes.string,
|
||||||
|
trackedDownloadState: PropTypes.string,
|
||||||
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
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,
|
||||||
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
timeleft: PropTypes.string,
|
||||||
|
size: PropTypes.number,
|
||||||
|
year: PropTypes.number,
|
||||||
|
sizeleft: PropTypes.number,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
|
grabError: PropTypes.object,
|
||||||
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
|
onGrabPress: PropTypes.func.isRequired,
|
||||||
|
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
||||||
|
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueRow.defaultProps = {
|
||||||
|
customFormats: [],
|
||||||
|
isGrabbing: false,
|
||||||
|
isRemoving: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueRow;
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
|
||||||
import { Error } from 'App/State/AppSectionState';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
|
||||||
import Language from 'Language/Language';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
|
||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
|
||||||
import useMovie from 'Movie/useMovie';
|
|
||||||
import { QualityModel } from 'Quality/Quality';
|
|
||||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
|
||||||
import {
|
|
||||||
QueueTrackedDownloadState,
|
|
||||||
QueueTrackedDownloadStatus,
|
|
||||||
StatusMessage,
|
|
||||||
} from 'typings/Queue';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueueStatusCell from './QueueStatusCell';
|
|
||||||
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
|
||||||
import TimeleftCell from './TimeleftCell';
|
|
||||||
import styles from './QueueRow.css';
|
|
||||||
|
|
||||||
interface QueueRowProps {
|
|
||||||
id: number;
|
|
||||||
movieId?: number;
|
|
||||||
downloadId?: string;
|
|
||||||
title: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
|
||||||
trackedDownloadState?: QueueTrackedDownloadState;
|
|
||||||
statusMessages?: StatusMessage[];
|
|
||||||
errorMessage?: string;
|
|
||||||
languages: Language[];
|
|
||||||
quality: QualityModel;
|
|
||||||
customFormats?: CustomFormat[];
|
|
||||||
customFormatScore: number;
|
|
||||||
protocol: DownloadProtocol;
|
|
||||||
indexer?: string;
|
|
||||||
outputPath?: string;
|
|
||||||
downloadClient?: string;
|
|
||||||
downloadClientHasPostImportCategory?: boolean;
|
|
||||||
estimatedCompletionTime?: string;
|
|
||||||
added?: string;
|
|
||||||
timeleft?: string;
|
|
||||||
size: number;
|
|
||||||
sizeleft: number;
|
|
||||||
isGrabbing?: boolean;
|
|
||||||
grabError?: Error;
|
|
||||||
isRemoving?: boolean;
|
|
||||||
isSelected?: boolean;
|
|
||||||
columns: Column[];
|
|
||||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
|
||||||
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function QueueRow(props: QueueRowProps) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
movieId,
|
|
||||||
downloadId,
|
|
||||||
title,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
trackedDownloadState,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage,
|
|
||||||
languages,
|
|
||||||
quality,
|
|
||||||
customFormats = [],
|
|
||||||
customFormatScore,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
outputPath,
|
|
||||||
downloadClient,
|
|
||||||
downloadClientHasPostImportCategory,
|
|
||||||
estimatedCompletionTime,
|
|
||||||
added,
|
|
||||||
timeleft,
|
|
||||||
size,
|
|
||||||
sizeleft,
|
|
||||||
isGrabbing = false,
|
|
||||||
grabError,
|
|
||||||
isRemoving = false,
|
|
||||||
isSelected,
|
|
||||||
columns,
|
|
||||||
onSelectedChange,
|
|
||||||
onQueueRowModalOpenOrClose,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const movie = useMovie(movieId);
|
|
||||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
|
||||||
createUISettingsSelector()
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const handleGrabPress = useCallback(() => {
|
|
||||||
dispatch(grabQueueItem({ id }));
|
|
||||||
}, [id, dispatch]);
|
|
||||||
|
|
||||||
const handleInteractiveImportPress = useCallback(() => {
|
|
||||||
onQueueRowModalOpenOrClose(true);
|
|
||||||
setIsInteractiveImportModalOpen(true);
|
|
||||||
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
|
||||||
|
|
||||||
const handleInteractiveImportModalClose = useCallback(() => {
|
|
||||||
onQueueRowModalOpenOrClose(false);
|
|
||||||
setIsInteractiveImportModalOpen(false);
|
|
||||||
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
|
||||||
|
|
||||||
const handleRemoveQueueItemPress = useCallback(() => {
|
|
||||||
onQueueRowModalOpenOrClose(true);
|
|
||||||
setIsRemoveQueueItemModalOpen(true);
|
|
||||||
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
|
||||||
|
|
||||||
const handleRemoveQueueItemModalConfirmed = useCallback(
|
|
||||||
(payload: RemovePressProps) => {
|
|
||||||
onQueueRowModalOpenOrClose(false);
|
|
||||||
dispatch(removeQueueItem({ id, ...payload }));
|
|
||||||
setIsRemoveQueueItemModalOpen(false);
|
|
||||||
},
|
|
||||||
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemoveQueueItemModalClose = useCallback(() => {
|
|
||||||
onQueueRowModalOpenOrClose(false);
|
|
||||||
setIsRemoveQueueItemModalOpen(false);
|
|
||||||
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
|
||||||
|
|
||||||
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
|
||||||
const showInteractiveImport =
|
|
||||||
status === 'completed' && trackedDownloadStatus === 'warning';
|
|
||||||
const isPending =
|
|
||||||
status === 'delay' || status === 'downloadClientUnavailable';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{columns.map((column) => {
|
|
||||||
const { name, isVisible } = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
|
||||||
return (
|
|
||||||
<QueueStatusCell
|
|
||||||
key={name}
|
|
||||||
sourceTitle={title}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'movies.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{movie ? (
|
|
||||||
<MovieTitleLink
|
|
||||||
titleSlug={movie.titleSlug}
|
|
||||||
title={movie.title}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
title
|
|
||||||
)}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'year') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>{movie ? movie.year : ''}</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieLanguages languages={languages} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{quality ? <MovieQuality quality={quality} /> : null}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieFormats formats={customFormats} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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}>
|
|
||||||
<ProtocolLabel protocol={protocol} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return <TableRowCell key={name}>{indexer}</TableRowCell>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'title') {
|
|
||||||
return <TableRowCell key={name}>{title}</TableRowCell>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'size') {
|
|
||||||
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'outputPath') {
|
|
||||||
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'estimatedCompletionTime') {
|
|
||||||
return (
|
|
||||||
<TimeleftCell
|
|
||||||
key={name}
|
|
||||||
status={status}
|
|
||||||
estimatedCompletionTime={estimatedCompletionTime}
|
|
||||||
timeleft={timeleft}
|
|
||||||
size={size}
|
|
||||||
sizeleft={sizeleft}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'progress') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name} className={styles.progress}>
|
|
||||||
{!!progress && (
|
|
||||||
<ProgressBar
|
|
||||||
progress={progress}
|
|
||||||
title={`${progress.toFixed(1)}%`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'added') {
|
|
||||||
return <RelativeDateCell key={name} date={added} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name} className={styles.actions}>
|
|
||||||
{showInteractiveImport ? (
|
|
||||||
<IconButton
|
|
||||||
name={icons.INTERACTIVE}
|
|
||||||
onPress={handleInteractiveImportPress}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isPending ? (
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={icons.DOWNLOAD}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={handleGrabPress}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<SpinnerIconButton
|
|
||||||
title={translate('RemoveFromQueue')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={handleRemoveQueueItemPress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
|
|
||||||
<InteractiveImportModal
|
|
||||||
isOpen={isInteractiveImportModalOpen}
|
|
||||||
downloadId={downloadId}
|
|
||||||
modalTitle={title}
|
|
||||||
onModalClose={handleInteractiveImportModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RemoveQueueItemModal
|
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
|
||||||
sourceTitle={title}
|
|
||||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
|
||||||
canIgnore={!!movie}
|
|
||||||
isPending={isPending}
|
|
||||||
onRemovePress={handleRemoveQueueItemModalConfirmed}
|
|
||||||
onModalClose={handleRemoveQueueItemModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QueueRow;
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import QueueRow from './QueueRow';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createMovieSelector(),
|
||||||
|
createUISettingsSelector(),
|
||||||
|
(movie, uiSettings) => {
|
||||||
|
const result = {
|
||||||
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
result.movie = movie;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
grabQueueItem,
|
||||||
|
removeQueueItem
|
||||||
|
};
|
||||||
|
|
||||||
|
class QueueRowConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onGrabPress = () => {
|
||||||
|
this.props.grabQueueItem({ id: this.props.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveQueueItemPress = (payload) => {
|
||||||
|
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<QueueRow
|
||||||
|
{...this.props}
|
||||||
|
onGrabPress={this.onGrabPress}
|
||||||
|
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueRowConnector.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
movie: PropTypes.object,
|
||||||
|
grabQueueItem: PropTypes.func.isRequired,
|
||||||
|
removeQueueItem: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
||||||
+58
-54
@@ -1,59 +1,51 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon, { IconProps } from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
|
||||||
import {
|
|
||||||
QueueTrackedDownloadState,
|
|
||||||
QueueTrackedDownloadStatus,
|
|
||||||
StatusMessage,
|
|
||||||
} from 'typings/Queue';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './QueueStatus.css';
|
import styles from './QueueStatus.css';
|
||||||
|
|
||||||
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
|
function getDetailedPopoverBody(statusMessages) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{statusMessages.map(({ title, messages }) => {
|
{
|
||||||
return (
|
statusMessages.map(({ title, messages }) => {
|
||||||
<div
|
return (
|
||||||
key={title}
|
<div
|
||||||
className={messages.length ? undefined : styles.noMessages}
|
key={title}
|
||||||
>
|
className={messages.length ? undefined: styles.noMessages}
|
||||||
{title}
|
>
|
||||||
<ul>
|
{title}
|
||||||
{messages.map((message) => {
|
<ul>
|
||||||
return <li key={message}>{message}</li>;
|
{
|
||||||
})}
|
messages.map((message) => {
|
||||||
</ul>
|
return (
|
||||||
</div>
|
<li key={message}>
|
||||||
);
|
{message}
|
||||||
})}
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueStatusProps {
|
function QueueStatus(props) {
|
||||||
sourceTitle: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
|
||||||
trackedDownloadState?: QueueTrackedDownloadState;
|
|
||||||
statusMessages?: StatusMessage[];
|
|
||||||
errorMessage?: string;
|
|
||||||
position: TooltipPosition;
|
|
||||||
canFlip?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function QueueStatus(props: QueueStatusProps) {
|
|
||||||
const {
|
const {
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
status,
|
status,
|
||||||
trackedDownloadStatus = 'ok',
|
trackedDownloadStatus,
|
||||||
trackedDownloadState = 'downloading',
|
trackedDownloadState,
|
||||||
statusMessages = [],
|
statusMessages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
position,
|
position,
|
||||||
canFlip = false,
|
canFlip
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
@@ -61,7 +53,7 @@ function QueueStatus(props: QueueStatusProps) {
|
|||||||
|
|
||||||
// status === 'downloading'
|
// status === 'downloading'
|
||||||
let iconName = icons.DOWNLOADING;
|
let iconName = icons.DOWNLOADING;
|
||||||
let iconKind: IconProps['kind'] = kinds.DEFAULT;
|
let iconKind = kinds.DEFAULT;
|
||||||
let title = translate('Downloading');
|
let title = translate('Downloading');
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
@@ -78,11 +70,6 @@ function QueueStatus(props: QueueStatusProps) {
|
|||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = translate('Downloaded');
|
title = translate('Downloaded');
|
||||||
|
|
||||||
if (trackedDownloadState === 'importBlocked') {
|
|
||||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
if (trackedDownloadState === 'importPending') {
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
title += ` - ${translate('WaitingToImport')}`;
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
@@ -123,8 +110,7 @@ function QueueStatus(props: QueueStatusProps) {
|
|||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage =
|
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||||
errorMessage || translate('CheckDownloadClientForDetails');
|
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
title = translate('DownloadWarning', { warningMessage });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,17 +128,35 @@ function QueueStatus(props: QueueStatusProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
anchor={<Icon name={iconName} kind={iconKind} />}
|
anchor={
|
||||||
title={title}
|
<Icon
|
||||||
body={
|
name={iconName}
|
||||||
hasWarning || hasError
|
kind={iconKind}
|
||||||
? getDetailedPopoverBody(statusMessages)
|
/>
|
||||||
: sourceTitle
|
|
||||||
}
|
}
|
||||||
|
title={title}
|
||||||
|
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||||
position={position}
|
position={position}
|
||||||
canFlip={canFlip}
|
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;
|
export default QueueStatus;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueueStatus from './QueueStatus';
|
||||||
|
import styles from './QueueStatusCell.css';
|
||||||
|
|
||||||
|
function QueueStatusCell(props) {
|
||||||
|
const {
|
||||||
|
sourceTitle,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.status}>
|
||||||
|
<QueueStatus
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueStatusCell.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
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueStatusCell.defaultProps = {
|
||||||
|
trackedDownloadStatus: translate('Ok'),
|
||||||
|
trackedDownloadState: translate('Downloading')
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueStatusCell;
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import {
|
|
||||||
QueueTrackedDownloadState,
|
|
||||||
QueueTrackedDownloadStatus,
|
|
||||||
StatusMessage,
|
|
||||||
} from 'typings/Queue';
|
|
||||||
import QueueStatus from './QueueStatus';
|
|
||||||
import styles from './QueueStatusCell.css';
|
|
||||||
|
|
||||||
interface QueueStatusCellProps {
|
|
||||||
sourceTitle: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
|
||||||
trackedDownloadState?: QueueTrackedDownloadState;
|
|
||||||
statusMessages?: StatusMessage[];
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function QueueStatusCell(props: QueueStatusCellProps) {
|
|
||||||
const {
|
|
||||||
sourceTitle,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus = 'ok',
|
|
||||||
trackedDownloadState = 'downloading',
|
|
||||||
statusMessages,
|
|
||||||
errorMessage,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell className={styles.status}>
|
|
||||||
<QueueStatus
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
position="right"
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QueueStatusCell;
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
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,
|
||||||
|
skipRedownload: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
resetState = function() {
|
||||||
|
this.setState({
|
||||||
|
remove: true,
|
||||||
|
blocklist: false,
|
||||||
|
skipRedownload: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRemoveChange = ({ value }) => {
|
||||||
|
this.setState({ remove: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlocklistChange = ({ value }) => {
|
||||||
|
this.setState({ blocklist: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSkipRedownloadChange = ({ value }) => {
|
||||||
|
this.setState({ skipRedownload: 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, skipRedownload } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('RemoveQueueItem', { sourceTitle })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isPending ?
|
||||||
|
null :
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="remove"
|
||||||
|
value={remove}
|
||||||
|
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||||
|
isDisabled={!canIgnore}
|
||||||
|
onChange={this.onRemoveChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="blocklist"
|
||||||
|
value={blocklist}
|
||||||
|
helpText={translate('BlocklistReleaseHelpText')}
|
||||||
|
onChange={this.onBlocklistChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
blocklist ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="skipRedownload"
|
||||||
|
value={skipRedownload}
|
||||||
|
helpText={translate('SkipRedownloadHelpText')}
|
||||||
|
onChange={this.onSkipRedownloadChange}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={this.onModalClose}>
|
||||||
|
{translate('Close')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={this.onRemoveConfirmed}
|
||||||
|
>
|
||||||
|
{translate('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';
|
|
||||||
|
|
||||||
export 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;
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
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,
|
||||||
|
skipRedownload: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
resetState = function() {
|
||||||
|
this.setState({
|
||||||
|
remove: true,
|
||||||
|
blocklist: false,
|
||||||
|
skipRedownload: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRemoveChange = ({ value }) => {
|
||||||
|
this.setState({ remove: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlocklistChange = ({ value }) => {
|
||||||
|
this.setState({ blocklist: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSkipRedownloadChange = ({ value }) => {
|
||||||
|
this.setState({ skipRedownload: 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, skipRedownload } = 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('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||||
|
</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('BlocklistReleaseHelpText')}
|
||||||
|
onChange={this.onBlocklistChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
blocklist ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="skipRedownload"
|
||||||
|
value={skipRedownload}
|
||||||
|
helpText={translate('SkipRedownloadHelpText')}
|
||||||
|
onChange={this.onSkipRedownloadChange}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={this.onModalClose}>
|
||||||
|
{translate('Close')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={this.onRemoveConfirmed}
|
||||||
|
>
|
||||||
|
{translate('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,37 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
|
||||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
|
||||||
import createQueueStatusSelector from './createQueueStatusSelector';
|
|
||||||
|
|
||||||
function QueueStatus() {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { isConnected, isReconnecting } = useSelector(
|
|
||||||
(state: AppState) => state.app
|
|
||||||
);
|
|
||||||
const { isPopulated, count, errors, warnings } = useSelector(
|
|
||||||
createQueueStatusSelector()
|
|
||||||
);
|
|
||||||
|
|
||||||
const wasReconnecting = usePrevious(isReconnecting);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPopulated) {
|
|
||||||
dispatch(fetchQueueStatus());
|
|
||||||
}
|
|
||||||
}, [isPopulated, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isConnected && wasReconnecting) {
|
|
||||||
dispatch(fetchQueueStatus());
|
|
||||||
}
|
|
||||||
}, [isConnected, wasReconnecting, dispatch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QueueStatus;
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||||
|
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.app,
|
||||||
|
(state) => state.queue.status,
|
||||||
|
(state) => state.queue.options.includeUnknownMovieItems,
|
||||||
|
(app, status, includeUnknownMovieItems) => {
|
||||||
|
const {
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
unknownErrors,
|
||||||
|
unknownWarnings,
|
||||||
|
count,
|
||||||
|
totalCount
|
||||||
|
} = status.item;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected: app.isConnected,
|
||||||
|
isReconnecting: app.isReconnecting,
|
||||||
|
isPopulated: status.isPopulated,
|
||||||
|
...status.item,
|
||||||
|
count: includeUnknownMovieItems ? totalCount : count,
|
||||||
|
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
||||||
|
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchQueueStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
class QueueStatusConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.fetchQueueStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||||
|
this.props.fetchQueueStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PageSidebarStatus
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueStatusConnector.propTypes = {
|
||||||
|
isConnected: PropTypes.bool.isRequired,
|
||||||
|
isReconnecting: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
fetchQueueStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
|
|
||||||
function createQueueStatusSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.queue.status.isPopulated,
|
|
||||||
(state: AppState) => state.queue.status.item,
|
|
||||||
(state: AppState) => state.queue.options.includeUnknownMovieItems,
|
|
||||||
(isPopulated, status, includeUnknownMovieItems) => {
|
|
||||||
const {
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
unknownErrors,
|
|
||||||
unknownWarnings,
|
|
||||||
count,
|
|
||||||
totalCount,
|
|
||||||
} = status;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...status,
|
|
||||||
isPopulated,
|
|
||||||
count: includeUnknownMovieItems ? totalCount : count,
|
|
||||||
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
|
||||||
warnings: includeUnknownMovieItems
|
|
||||||
? warnings || unknownWarnings
|
|
||||||
: warnings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createQueueStatusSelector;
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import formatTime from 'Utilities/Date/formatTime';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TimeleftCell.css';
|
||||||
|
|
||||||
|
function TimeleftCell(props) {
|
||||||
|
const {
|
||||||
|
estimatedCompletionTime,
|
||||||
|
timeleft,
|
||||||
|
status,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (status === 'delay') {
|
||||||
|
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.timeleft}
|
||||||
|
title={translate('DelayingDownloadUntil', { date, time })}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'downloadClientUnavailable') {
|
||||||
|
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.timeleft}
|
||||||
|
title={translate('RetryingDownloadOn', { date, time })}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.timeleft}>
|
||||||
|
-
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = formatBytes(size);
|
||||||
|
const remainingSize = formatBytes(sizeleft);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.timeleft}
|
||||||
|
title={`${remainingSize} / ${totalSize}`}
|
||||||
|
>
|
||||||
|
{formatTimeSpan(timeleft)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeleftCell.propTypes = {
|
||||||
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
timeleft: PropTypes.string,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
sizeleft: PropTypes.number.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeleftCell;
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
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';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './TimeleftCell.css';
|
|
||||||
|
|
||||||
interface TimeleftCellProps {
|
|
||||||
estimatedCompletionTime?: string;
|
|
||||||
timeleft?: string;
|
|
||||||
status: string;
|
|
||||||
size: number;
|
|
||||||
sizeleft: number;
|
|
||||||
showRelativeDates: boolean;
|
|
||||||
shortDateFormat: string;
|
|
||||||
timeFormat: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TimeleftCell(props: TimeleftCellProps) {
|
|
||||||
const {
|
|
||||||
estimatedCompletionTime,
|
|
||||||
timeleft,
|
|
||||||
status,
|
|
||||||
size,
|
|
||||||
sizeleft,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (status === 'delay') {
|
|
||||||
const date = getRelativeDate({
|
|
||||||
date: estimatedCompletionTime,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
});
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
|
||||||
const date = getRelativeDate({
|
|
||||||
date: estimatedCompletionTime,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
|
||||||
});
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
|
||||||
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalSize = formatBytes(size);
|
|
||||||
const remainingSize = formatBytes(sizeleft);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.timeleft}
|
|
||||||
title={`${remainingSize} / ${totalSize}`}
|
|
||||||
>
|
|
||||||
{formatTimeSpan(timeleft)}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TimeleftCell;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
@@ -131,12 +130,7 @@ class AddNewMovie extends Component {
|
|||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
|
<div>{getErrorMessage(error)}</div>
|
||||||
<div>
|
|
||||||
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
|
||||||
{translate('WhySearchesCouldBeFailing')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
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 parseUrl from 'Utilities/String/parseUrl';
|
||||||
import AddNewMovie from './AddNewMovie';
|
import AddNewMovie from './AddNewMovie';
|
||||||
|
|
||||||
@@ -35,10 +33,9 @@ const mapDispatchToProps = {
|
|||||||
lookupMovie,
|
lookupMovie,
|
||||||
clearAddMovie,
|
clearAddMovie,
|
||||||
fetchRootFolders,
|
fetchRootFolders,
|
||||||
|
fetchImportExclusions,
|
||||||
fetchQueueDetails,
|
fetchQueueDetails,
|
||||||
clearQueueDetails,
|
clearQueueDetails
|
||||||
fetchMovieFiles,
|
|
||||||
clearMovieFiles
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class AddNewMovieConnector extends Component {
|
class AddNewMovieConnector extends Component {
|
||||||
@@ -54,23 +51,10 @@ class AddNewMovieConnector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchRootFolders();
|
this.props.fetchRootFolders();
|
||||||
|
this.props.fetchImportExclusions();
|
||||||
this.props.fetchQueueDetails();
|
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() {
|
componentWillUnmount() {
|
||||||
if (this._movieLookupTimeout) {
|
if (this._movieLookupTimeout) {
|
||||||
clearTimeout(this._movieLookupTimeout);
|
clearTimeout(this._movieLookupTimeout);
|
||||||
@@ -78,7 +62,6 @@ class AddNewMovieConnector extends Component {
|
|||||||
|
|
||||||
this.props.clearAddMovie();
|
this.props.clearAddMovie();
|
||||||
this.props.clearQueueDetails();
|
this.props.clearQueueDetails();
|
||||||
this.props.clearMovieFiles();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -124,14 +107,12 @@ class AddNewMovieConnector extends Component {
|
|||||||
|
|
||||||
AddNewMovieConnector.propTypes = {
|
AddNewMovieConnector.propTypes = {
|
||||||
term: PropTypes.string,
|
term: PropTypes.string,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
lookupMovie: PropTypes.func.isRequired,
|
lookupMovie: PropTypes.func.isRequired,
|
||||||
clearAddMovie: PropTypes.func.isRequired,
|
clearAddMovie: PropTypes.func.isRequired,
|
||||||
fetchRootFolders: PropTypes.func.isRequired,
|
fetchRootFolders: PropTypes.func.isRequired,
|
||||||
|
fetchImportExclusions: PropTypes.func.isRequired,
|
||||||
fetchQueueDetails: PropTypes.func.isRequired,
|
fetchQueueDetails: PropTypes.func.isRequired,
|
||||||
clearQueueDetails: PropTypes.func.isRequired,
|
clearQueueDetails: PropTypes.func.isRequired
|
||||||
fetchMovieFiles: PropTypes.func.isRequired,
|
|
||||||
clearMovieFiles: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
|
||||||
|
|||||||
@@ -85,14 +85,8 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.originalLanguage,
|
|
||||||
.studio,
|
|
||||||
.genres {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
margin-left: 5px;
|
margin-left: 8px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ interface CssExports {
|
|||||||
'certification': string;
|
'certification': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
'exclusionIcon': string;
|
'exclusionIcon': string;
|
||||||
'genres': string;
|
|
||||||
'icons': string;
|
'icons': string;
|
||||||
'links': string;
|
'links': string;
|
||||||
'originalLanguage': string;
|
|
||||||
'overlay': string;
|
'overlay': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
@@ -16,7 +14,6 @@ interface CssExports {
|
|||||||
'runtime': string;
|
'runtime': string;
|
||||||
'searchResult': string;
|
'searchResult': string;
|
||||||
'statusContainer': string;
|
'statusContainer': string;
|
||||||
'studio': string;
|
|
||||||
'title': string;
|
'title': string;
|
||||||
'titleContainer': string;
|
'titleContainer': string;
|
||||||
'titleRow': string;
|
'titleRow': string;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
@@ -62,23 +61,21 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
titleSlug,
|
titleSlug,
|
||||||
year,
|
year,
|
||||||
studio,
|
studio,
|
||||||
originalLanguage,
|
|
||||||
genres,
|
|
||||||
status,
|
status,
|
||||||
overview,
|
overview,
|
||||||
ratings,
|
ratings,
|
||||||
folder,
|
folder,
|
||||||
images,
|
images,
|
||||||
existingMovieId,
|
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isExcluded,
|
isExclusionMovie,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
colorImpairedMode,
|
colorImpairedMode,
|
||||||
id,
|
id,
|
||||||
monitored,
|
monitored,
|
||||||
|
hasFile,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
movieFile,
|
queueStatus,
|
||||||
queueItem,
|
queueState,
|
||||||
runtime,
|
runtime,
|
||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
certification
|
certification
|
||||||
@@ -88,8 +85,6 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
isNewAddMovieModalOpen
|
isNewAddMovieModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const hasMovieFile = !!movieFile;
|
|
||||||
|
|
||||||
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
||||||
const posterWidth = 167;
|
const posterWidth = 167;
|
||||||
const posterHeight = 250;
|
const posterHeight = 250;
|
||||||
@@ -125,13 +120,13 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
{
|
{
|
||||||
isExistingMovie &&
|
isExistingMovie &&
|
||||||
<MovieIndexProgressBar
|
<MovieIndexProgressBar
|
||||||
movieId={existingMovieId}
|
|
||||||
movieFile={movieFile}
|
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
hasFile={hasMovieFile}
|
hasFile={hasFile}
|
||||||
status={status}
|
status={status}
|
||||||
width={posterWidth}
|
width={posterWidth}
|
||||||
detailedProgressBar={true}
|
detailedProgressBar={true}
|
||||||
|
queueStatus={queueStatus}
|
||||||
|
queueState={queueState}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -155,27 +150,26 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.icons}>
|
<div className={styles.icons}>
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isExistingMovie &&
|
|
||||||
<Icon
|
|
||||||
className={styles.alreadyExistsIcon}
|
|
||||||
name={icons.CHECK_CIRCLE}
|
|
||||||
size={36}
|
|
||||||
title={translate('AlreadyInYourLibrary')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
isExcluded &&
|
isExistingMovie &&
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.exclusionIcon}
|
className={styles.alreadyExistsIcon}
|
||||||
name={icons.DANGER}
|
name={icons.CHECK_CIRCLE}
|
||||||
size={36}
|
size={36}
|
||||||
title={translate('MovieIsOnImportExclusionList')}
|
title={translate('AlreadyInYourLibrary')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
|
{
|
||||||
|
isExclusionMovie &&
|
||||||
|
<Icon
|
||||||
|
className={styles.exclusionIcon}
|
||||||
|
name={icons.DANGER}
|
||||||
|
size={36}
|
||||||
|
title={translate('MovieIsOnImportExclusionList')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -204,56 +198,10 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{
|
{
|
||||||
ratings.imdb ?
|
!!studio &&
|
||||||
<Label size={sizes.LARGE}>
|
<Label size={sizes.LARGE}>
|
||||||
<ImdbRating
|
{studio}
|
||||||
ratings={ratings}
|
</Label>
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -267,15 +215,15 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
{translate('Links')}
|
Links
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
<MovieDetailsLinks
|
<MovieDetailsLinks
|
||||||
tmdbId={tmdbId}
|
tmdbId={tmdbId}
|
||||||
imdbId={imdbId}
|
|
||||||
youTubeTrailerId={youTubeTrailerId}
|
youTubeTrailerId={youTubeTrailerId}
|
||||||
|
imdbId={imdbId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
canFlip={true}
|
canFlip={true}
|
||||||
@@ -286,11 +234,9 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
{
|
{
|
||||||
isExistingMovie && isSmallScreen &&
|
isExistingMovie && isSmallScreen &&
|
||||||
<MovieStatusLabel
|
<MovieStatusLabel
|
||||||
status={status}
|
hasMovieFiles={hasFile}
|
||||||
hasMovieFiles={hasMovieFile}
|
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
queueItem={queueItem}
|
|
||||||
id={id}
|
id={id}
|
||||||
useLabel={true}
|
useLabel={true}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
@@ -327,31 +273,25 @@ AddNewMovieSearchResult.propTypes = {
|
|||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
studio: PropTypes.string,
|
studio: PropTypes.string,
|
||||||
originalLanguage: PropTypes.object,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
folder: PropTypes.string.isRequired,
|
folder: PropTypes.string.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
existingMovieId: PropTypes.number,
|
|
||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
isExcluded: PropTypes.bool,
|
isExclusionMovie: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
|
queueItems: PropTypes.arrayOf(PropTypes.object),
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
hasFile: PropTypes.bool.isRequired,
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
movieFile: PropTypes.object,
|
|
||||||
queueItem: PropTypes.object,
|
|
||||||
colorImpairedMode: PropTypes.bool,
|
colorImpairedMode: PropTypes.bool,
|
||||||
|
queueStatus: PropTypes.string,
|
||||||
|
queueState: PropTypes.string,
|
||||||
runtime: PropTypes.number.isRequired,
|
runtime: PropTypes.number.isRequired,
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||||
certification: PropTypes.string
|
certification: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
AddNewMovieSearchResult.defaultProps = {
|
|
||||||
genres: [],
|
|
||||||
isExcluded: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewMovieSearchResult;
|
export default AddNewMovieSearchResult;
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
|
||||||
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
||||||
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createExistingMovieSelector(),
|
createExistingMovieSelector(),
|
||||||
|
createExclusionMovieSelector(),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(state) => state.queue.details.items,
|
(state) => state.queue.details.items,
|
||||||
(state) => state.movieFiles.items,
|
|
||||||
(state, { internalId }) => internalId,
|
(state, { internalId }) => internalId,
|
||||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
|
||||||
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
|
||||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
|
||||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
existingMovieId: internalId,
|
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
|
isExclusionMovie,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
queueItem,
|
queueStatus: firstQueueItem ? firstQueueItem.status : null,
|
||||||
movieFile,
|
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
|
||||||
movieRuntimeFormat
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import styles from './ImportMovieRow.css';
|
|||||||
function ImportMovieRow(props) {
|
function ImportMovieRow(props) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
relativePath,
|
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
@@ -32,7 +31,7 @@ function ImportMovieRow(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.folder}>
|
<VirtualTableRowCell className={styles.folder}>
|
||||||
{relativePath}
|
{id}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.movie}>
|
<VirtualTableRowCell className={styles.movie}>
|
||||||
@@ -74,7 +73,6 @@ function ImportMovieRow(props) {
|
|||||||
|
|
||||||
ImportMovieRow.propTypes = {
|
ImportMovieRow.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
relativePath: PropTypes.string.isRequired,
|
|
||||||
monitor: PropTypes.string.isRequired,
|
monitor: PropTypes.string.isRequired,
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
minimumAvailability: PropTypes.string.isRequired,
|
minimumAvailability: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ImportMovieTable extends Component {
|
|||||||
unmappedFolders.forEach((unmappedFolder) => {
|
unmappedFolders.forEach((unmappedFolder) => {
|
||||||
const id = unmappedFolder.name;
|
const id = unmappedFolder.name;
|
||||||
|
|
||||||
onMovieLookup(id, unmappedFolder.path, unmappedFolder.relativePath);
|
onMovieLookup(id, unmappedFolder.path);
|
||||||
|
|
||||||
onSetImportMovieValue({
|
onSetImportMovieValue({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -25,11 +25,10 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
onMovieLookup(name, path, relativePath) {
|
onMovieLookup(name, path) {
|
||||||
dispatch(queueLookupMovie({
|
dispatch(queueLookupMovie({
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
relativePath,
|
|
||||||
term: name
|
term: name
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
.contentContainer {
|
.contentContainer {
|
||||||
z-index: $popperZIndex;
|
z-index: $popperZIndex;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
/* 400px container width with 8px padding on each side */
|
/* 400px container witdh with 8px padding on each side */
|
||||||
width: 384px;
|
width: 384px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
|
|||||||
className={styles.addErrorAlert}
|
className={styles.addErrorAlert}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{translate('AddRootFolderError')}
|
{translate('UnableToAddRootFolder')}
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
|
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createRootFoldersSelector(),
|
(state) => state.rootFolders,
|
||||||
createSystemStatusSelector(),
|
createSystemStatusSelector(),
|
||||||
(rootFolders, systemStatus) => {
|
(rootFolders, systemStatus) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ function App({ store, history }) {
|
|||||||
<DocumentTitle title={window.Radarr.instanceName}>
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme />
|
<ApplyTheme>
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes app={App} />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
|
</ApplyTheme>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||||
import Queue from 'Activity/Queue/Queue';
|
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
@@ -33,8 +33,6 @@ import Status from 'System/Status/Status';
|
|||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
|
||||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
|
||||||
|
|
||||||
function AppRoutes(props) {
|
function AppRoutes(props) {
|
||||||
const {
|
const {
|
||||||
@@ -115,26 +113,12 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/activity/queue"
|
path="/activity/queue"
|
||||||
component={Queue}
|
component={QueueConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/activity/blocklist"
|
path="/activity/blocklist"
|
||||||
component={Blocklist}
|
component={BlocklistConnector}
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Wanted
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/missing"
|
|
||||||
component={MissingConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/cutoffunmet"
|
|
||||||
component={CutoffUnmetConnector}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
|
|||||||
@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('AppUpdated')}
|
{translate('AppUpdated', { appName: 'Radarr' })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
{translate('ConnectionLostToBackend')}
|
{translate('ConnectionLostToBackend', { appName: 'Radarr' })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostReconnect')}
|
{translate('ConnectionLostReconnect', { appName: 'Radarr' })}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Column from 'Components/Table/Column';
|
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
import { FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
@@ -19,18 +18,10 @@ export interface AppSectionSaveState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagedAppSectionState {
|
export interface PagedAppSectionState {
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
totalPages: number;
|
|
||||||
totalRecords?: number;
|
|
||||||
}
|
|
||||||
export interface TableAppSectionState {
|
|
||||||
columns: Column[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionFilterState<T> {
|
export interface AppSectionFilterState<T> {
|
||||||
selectedFilterKey: string;
|
|
||||||
filters: PropertyFilter[];
|
|
||||||
filterBuilderProps: FilterBuilderProp<T>[];
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +38,6 @@ export interface AppSectionItemState<T> {
|
|||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
pendingChanges: Partial<T>;
|
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
@@ -45,20 +44,7 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
|
||||||
isConnected: boolean;
|
|
||||||
isReconnecting: boolean;
|
|
||||||
version: string;
|
|
||||||
dimensions: {
|
|
||||||
isSmallScreen: boolean;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
app: AppSectionState;
|
|
||||||
blocklist: BlocklistAppState;
|
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import Blocklist from 'typings/Blocklist';
|
|
||||||
import AppSectionState, {
|
|
||||||
AppSectionFilterState,
|
|
||||||
PagedAppSectionState,
|
|
||||||
TableAppSectionState,
|
|
||||||
} from './AppSectionState';
|
|
||||||
|
|
||||||
interface BlocklistAppState
|
|
||||||
extends AppSectionState<Blocklist>,
|
|
||||||
AppSectionFilterState<Blocklist>,
|
|
||||||
PagedAppSectionState,
|
|
||||||
TableAppSectionState {
|
|
||||||
isRemoving: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BlocklistAppState;
|
|
||||||
@@ -20,14 +20,11 @@ export interface MovieIndexAppState {
|
|||||||
showTitle: boolean;
|
showTitle: boolean;
|
||||||
showMonitored: boolean;
|
showMonitored: boolean;
|
||||||
showQualityProfile: boolean;
|
showQualityProfile: boolean;
|
||||||
showCinemaRelease: boolean;
|
|
||||||
showDigitalRelease: boolean;
|
|
||||||
showPhysicalRelease: boolean;
|
|
||||||
showReleaseDate: boolean;
|
showReleaseDate: boolean;
|
||||||
|
showCinemaRelease: boolean;
|
||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
showTags: boolean;
|
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,7 +37,6 @@ export interface MovieIndexAppState {
|
|||||||
showAdded: boolean;
|
showAdded: boolean;
|
||||||
showPath: boolean;
|
showPath: boolean;
|
||||||
showSizeOnDisk: boolean;
|
showSizeOnDisk: boolean;
|
||||||
showTags: boolean;
|
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
import Queue from 'typings/Queue';
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
Error,
|
Error,
|
||||||
PagedAppSectionState,
|
|
||||||
TableAppSectionState,
|
|
||||||
} from './AppSectionState';
|
} from './AppSectionState';
|
||||||
|
|
||||||
export interface QueueStatus {
|
export interface StatusMessage {
|
||||||
totalCount: number;
|
title: string;
|
||||||
count: number;
|
messages: string[];
|
||||||
unknownCount: number;
|
}
|
||||||
errors: boolean;
|
|
||||||
warnings: boolean;
|
export interface Queue extends ModelBase {
|
||||||
unknownErrors: boolean;
|
languages: Language[];
|
||||||
unknownWarnings: boolean;
|
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> {
|
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
@@ -23,9 +41,7 @@ export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
|||||||
|
|
||||||
export interface QueuePagedAppState
|
export interface QueuePagedAppState
|
||||||
extends AppSectionState<Queue>,
|
extends AppSectionState<Queue>,
|
||||||
AppSectionFilterState<Queue>,
|
AppSectionFilterState<Queue> {
|
||||||
PagedAppSectionState,
|
|
||||||
TableAppSectionState {
|
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
grabError: Error;
|
grabError: Error;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
@@ -33,12 +49,9 @@ export interface QueuePagedAppState
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface QueueAppState {
|
interface QueueAppState {
|
||||||
status: AppSectionItemState<QueueStatus>;
|
status: AppSectionItemState<Queue>;
|
||||||
details: QueueDetailsAppState;
|
details: QueueDetailsAppState;
|
||||||
paged: QueuePagedAppState;
|
paged: QueuePagedAppState;
|
||||||
options: {
|
|
||||||
includeUnknownMovieItems: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default QueueAppState;
|
export default QueueAppState;
|
||||||
|
|||||||
@@ -3,30 +3,19 @@ import AppSectionState, {
|
|||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
PagedAppSectionState,
|
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
|
||||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
import IndexerFlag from 'typings/IndexerFlag';
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import General from 'typings/Settings/General';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {
|
|
||||||
isTestingAll: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeneralAppState
|
|
||||||
extends AppSectionItemState<General>,
|
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
@@ -37,9 +26,7 @@ export interface ImportListAppState
|
|||||||
export interface IndexerAppState
|
export interface IndexerAppState
|
||||||
extends AppSectionState<Indexer>,
|
extends AppSectionState<Indexer>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState {}
|
||||||
isTestingAll: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NotificationAppState
|
export interface NotificationAppState
|
||||||
extends AppSectionState<Notification>,
|
extends AppSectionState<Notification>,
|
||||||
@@ -49,34 +36,12 @@ export interface QualityProfilesAppState
|
|||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
|
||||||
extends AppSectionState<CustomFormat>,
|
|
||||||
AppSectionDeleteState,
|
|
||||||
AppSectionSaveState {}
|
|
||||||
|
|
||||||
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 IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
|
||||||
customFormats: CustomFormatAppState;
|
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
general: GeneralAppState;
|
|
||||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
|
||||||
importListOptions: ImportListOptionsSettingsAppState;
|
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import DiskSpace from 'typings/DiskSpace';
|
|
||||||
import Health from 'typings/Health';
|
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import Task from 'typings/Task';
|
import { AppSectionItemState } from './AppSectionState';
|
||||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
|
||||||
|
|
||||||
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
|
||||||
export type HealthAppState = AppSectionState<Health>;
|
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
export type TaskAppState = AppSectionState<Task>;
|
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
diskSpace: DiskSpaceAppState;
|
|
||||||
health: HealthAppState;
|
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
tasks: TaskAppState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemAppState;
|
export default SystemAppState;
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import moment from 'moment';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AgendaEvent.css';
|
import styles from './AgendaEvent.css';
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class AgendaEvent extends Component {
|
|||||||
startTime = moment(startTime);
|
startTime = moment(startTime);
|
||||||
const downloading = !!(queueItem || grabbed);
|
const downloading = !!(queueItem || grabbed);
|
||||||
const isMonitored = monitored;
|
const isMonitored = monitored;
|
||||||
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
|
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
|
||||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||||
const link = `/movie/${titleSlug}`;
|
const link = `/movie/${titleSlug}`;
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class AgendaEvent extends Component {
|
|||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.MOVIE_FILE}
|
name={icons.MOVIE_FILE}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
title={translate('QualityCutoffNotMet')}
|
title={translate('QualityCutoffHasNotBeenMet')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ class Calendar extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('UnableToLoadTheCalendar')}
|
||||||
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
|
|||||||
gotoCalendarToday
|
gotoCalendarToday
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchCalendar();
|
fetchCalendar();
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class CalendarPage extends Component {
|
|||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('ICalLink')}
|
label={translate('iCalLink')}
|
||||||
iconName={icons.CALENDAR}
|
iconName={icons.CALENDAR}
|
||||||
onPress={this.onGetCalendarLinkPress}
|
onPress={this.onGetCalendarLinkPress}
|
||||||
/>
|
/>
|
||||||
@@ -112,7 +112,7 @@ class CalendarPage extends Component {
|
|||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('RssSync')}
|
label={translate('RSSSync')}
|
||||||
iconName={icons.RSS}
|
iconName={icons.RSS}
|
||||||
isSpinning={isRssSyncExecuting}
|
isSpinning={isRssSyncExecuting}
|
||||||
onPress={onRssSyncPress}
|
onPress={onRssSyncPress}
|
||||||
@@ -180,7 +180,7 @@ class CalendarPage extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!movieError && movieIsPopulated && !hasMovie &&
|
!movieError && movieIsPopulated && !hasMovie &&
|
||||||
<NoMovie totalItems={0} />
|
<NoMovie />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
|
|||||||
if (view === calendarViews.WEEK) {
|
if (view === calendarViews.WEEK) {
|
||||||
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
||||||
} else if (view === calendarViews.FORECAST) {
|
} else if (view === calendarViews.FORECAST) {
|
||||||
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
|
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
|
|||||||
.statusContainer {
|
.statusContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&:global(.fullColor) {
|
|
||||||
filter: var(--calendarFullColorFilter)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import classNames from 'classnames';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
||||||
import styles from './CalendarEvent.css';
|
import styles from './CalendarEvent.css';
|
||||||
@@ -39,7 +39,7 @@ class CalendarEvent extends Component {
|
|||||||
|
|
||||||
const isDownloading = !!(queueItem || grabbed);
|
const isDownloading = !!(queueItem || grabbed);
|
||||||
const isMonitored = monitored;
|
const isMonitored = monitored;
|
||||||
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
|
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading);
|
||||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||||
const link = `/movie/${titleSlug}`;
|
const link = `/movie/${titleSlug}`;
|
||||||
const eventType = [];
|
const eventType = [];
|
||||||
@@ -76,18 +76,12 @@ class CalendarEvent extends Component {
|
|||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className={styles.statusContainer}>
|
||||||
className={classNames(
|
|
||||||
styles.statusContainer,
|
|
||||||
fullColorEvents && 'fullColor'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
{
|
||||||
queueItem ?
|
queueItem ?
|
||||||
<span className={styles.statusIcon}>
|
<span className={styles.statusIcon}>
|
||||||
<CalendarEventQueueDetails
|
<CalendarEventQueueDetails
|
||||||
{...queueItem}
|
{...queueItem}
|
||||||
fullColorEvents={fullColorEvents}
|
|
||||||
/>
|
/>
|
||||||
</span> :
|
</span> :
|
||||||
null
|
null
|
||||||
@@ -104,14 +98,12 @@ class CalendarEvent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
showCutoffUnmetIcon &&
|
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet ?
|
||||||
!!movieFile &&
|
|
||||||
movieFile.qualityCutoffNotMet ?
|
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.MOVIE_FILE}
|
name={icons.MOVIE_FILE}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
title={translate('QualityCutoffNotMet')}
|
title={translate('QualityCutoffHasNotBeenMet')}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class CalendarHeader extends Component {
|
|||||||
isDisabled={view === calendarViews.AGENDA}
|
isDisabled={view === calendarViews.AGENDA}
|
||||||
onPress={onTodayPress}
|
onPress={onTodayPress}
|
||||||
>
|
>
|
||||||
{translate('Today')}
|
Today
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ function Legend(props) {
|
|||||||
if (showCutoffUnmetIcon) {
|
if (showCutoffUnmetIcon) {
|
||||||
iconsToShow.push(
|
iconsToShow.push(
|
||||||
<LegendIconItem
|
<LegendIconItem
|
||||||
name={translate('CutoffNotMet')}
|
name={translate('CutoffUnmet')}
|
||||||
icon={icons.MOVIE_FILE}
|
icon={icons.MOVIE_FILE}
|
||||||
kind={kinds.WARNING}
|
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||||
fullColorEvents={fullColorEvents}
|
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
|
||||||
tooltip={translate('QualityCutoffNotMet')}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,4 @@
|
|||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
&:global(.fullColorEvents) {
|
|
||||||
filter: var(--calendarFullColorFilter)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
@@ -7,9 +6,9 @@ import styles from './LegendIconItem.css';
|
|||||||
function LegendIconItem(props) {
|
function LegendIconItem(props) {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
fullColorEvents,
|
|
||||||
icon,
|
icon,
|
||||||
kind,
|
kind,
|
||||||
|
darken,
|
||||||
tooltip
|
tooltip
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -19,11 +18,9 @@ function LegendIconItem(props) {
|
|||||||
title={tooltip}
|
title={tooltip}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={classNames(
|
className={styles.icon}
|
||||||
styles.icon,
|
|
||||||
fullColorEvents && 'fullColorEvents'
|
|
||||||
)}
|
|
||||||
name={icon}
|
name={icon}
|
||||||
|
darken={darken}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -34,10 +31,14 @@ function LegendIconItem(props) {
|
|||||||
|
|
||||||
LegendIconItem.propTypes = {
|
LegendIconItem.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
fullColorEvents: PropTypes.bool.isRequired,
|
|
||||||
icon: PropTypes.object.isRequired,
|
icon: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
|
darken: PropTypes.bool.isRequired,
|
||||||
tooltip: PropTypes.string.isRequired
|
tooltip: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LegendIconItem.defaultProps = {
|
||||||
|
darken: false
|
||||||
|
};
|
||||||
|
|
||||||
export default LegendIconItem;
|
export default LegendIconItem;
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showCutoffUnmetIcon"
|
name="showCutoffUnmetIcon"
|
||||||
value={showCutoffUnmetIcon}
|
value={showCutoffUnmetIcon}
|
||||||
helpText={translate('IconForCutoffUnmetHelpText')}
|
helpText={translate('ShowCutoffUnmetIconHelpText')}
|
||||||
onChange={this.onOptionInputChange}
|
onChange={this.onOptionInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -177,7 +177,7 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
values={weekColumnOptions}
|
values={weekColumnOptions}
|
||||||
value={calendarWeekColumnHeader}
|
value={calendarWeekColumnHeader}
|
||||||
onChange={this.onGlobalInputChange}
|
onChange={this.onGlobalInputChange}
|
||||||
helpText={translate('WeekColumnHeaderHelpText')}
|
helpText={translate('SettingsWeekColumnHeaderHelpText')}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
function getStatusStyle(hasFile, downloading, isMonitored, isAvailable) {
|
|
||||||
if (downloading) {
|
|
||||||
return 'queue';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasFile && isMonitored) {
|
|
||||||
return 'downloaded';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasFile && !isMonitored) {
|
|
||||||
return 'unmonitored';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAvailable && isMonitored) {
|
|
||||||
return 'missingMonitored';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMonitored) {
|
|
||||||
return 'missingUnmonitored';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'continuing';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getStatusStyle;
|
|
||||||
@@ -109,7 +109,7 @@ class CalendarLinkModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('CalendarFeed')}
|
{translate('RadarrCalendarFeed')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -121,19 +121,19 @@ class CalendarLinkModalContent extends Component {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="unmonitored"
|
name="unmonitored"
|
||||||
value={unmonitored}
|
value={unmonitored}
|
||||||
helpText={translate('ICalIncludeUnmonitoredMoviesHelpText')}
|
helpText={translate('UnmonitoredHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel>
|
<FormLabel>{translate('ShowAsAllDayEvents')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="asAllDay"
|
name="asAllDay"
|
||||||
value={asAllDay}
|
value={asAllDay}
|
||||||
helpText={translate('ICalShowAsAllDayEventsHelpText')}
|
helpText={translate('AsAllDayHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -145,7 +145,7 @@ class CalendarLinkModalContent extends Component {
|
|||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
helpText={translate('ICalTagsMoviesHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -160,7 +160,7 @@ class CalendarLinkModalContent extends Component {
|
|||||||
name="iCalHttpUrl"
|
name="iCalHttpUrl"
|
||||||
value={iCalHttpUrl}
|
value={iCalHttpUrl}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
helpText={translate('ICalFeedHelpText')}
|
helpText={translate('ICalHttpUrlHelpText')}
|
||||||
buttons={[
|
buttons={[
|
||||||
<ClipboardButton
|
<ClipboardButton
|
||||||
key="copy"
|
key="copy"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import * as commandNames from 'Commands/commandNames';
|
|||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
|
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
|
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
|
||||||
@@ -39,12 +38,6 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchRootFolders() {
|
dispatchFetchRootFolders() {
|
||||||
dispatch(fetchRootFolders());
|
dispatch(fetchRootFolders());
|
||||||
},
|
},
|
||||||
dispatchFetchQueueDetails() {
|
|
||||||
dispatch(fetchQueueDetails());
|
|
||||||
},
|
|
||||||
dispatchClearQueueDetails() {
|
|
||||||
dispatch(clearQueueDetails());
|
|
||||||
},
|
|
||||||
onUpdateSelectedPress(payload) {
|
onUpdateSelectedPress(payload) {
|
||||||
dispatch(saveMovieCollections(payload));
|
dispatch(saveMovieCollections(payload));
|
||||||
},
|
},
|
||||||
@@ -70,12 +63,10 @@ class CollectionConnector extends Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate);
|
||||||
this.props.dispatchFetchRootFolders();
|
this.props.dispatchFetchRootFolders();
|
||||||
this.props.dispatchFetchQueueDetails();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
unregisterPagePopulator(this.repopulate);
|
unregisterPagePopulator(this.repopulate);
|
||||||
this.props.dispatchClearQueueDetails();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -108,9 +99,7 @@ CollectionConnector.propTypes = {
|
|||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
view: PropTypes.string.isRequired,
|
view: PropTypes.string.isRequired,
|
||||||
onUpdateSelectedPress: PropTypes.func.isRequired,
|
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
dispatchFetchRootFolders: PropTypes.func.isRequired
|
||||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
|
||||||
dispatchClearQueueDetails: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withScrollPosition(
|
export default withScrollPosition(
|
||||||
|
|||||||
@@ -14,50 +14,6 @@ import styles from './CollectionFooter.css';
|
|||||||
|
|
||||||
const NO_CHANGE = 'noChange';
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
const monitoredOptions = [
|
|
||||||
{
|
|
||||||
key: NO_CHANGE,
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'monitored',
|
|
||||||
get value() {
|
|
||||||
return translate('Monitored');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'unmonitored',
|
|
||||||
get value() {
|
|
||||||
return translate('Unmonitored');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const searchOnAddOptions = [
|
|
||||||
{
|
|
||||||
key: NO_CHANGE,
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'yes',
|
|
||||||
get value() {
|
|
||||||
return translate('Yes');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'no',
|
|
||||||
get value() {
|
|
||||||
return translate('No');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class CollectionFooter extends Component {
|
class CollectionFooter extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -67,12 +23,12 @@ class CollectionFooter extends Component {
|
|||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
monitored: NO_CHANGE,
|
|
||||||
monitor: NO_CHANGE,
|
monitor: NO_CHANGE,
|
||||||
|
monitored: NO_CHANGE,
|
||||||
qualityProfileId: NO_CHANGE,
|
qualityProfileId: NO_CHANGE,
|
||||||
minimumAvailability: NO_CHANGE,
|
minimumAvailability: NO_CHANGE,
|
||||||
rootFolderPath: NO_CHANGE,
|
rootFolderPath: NO_CHANGE,
|
||||||
searchOnAdd: NO_CHANGE
|
destinationRootFolder: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +44,8 @@ class CollectionFooter extends Component {
|
|||||||
monitored: NO_CHANGE,
|
monitored: NO_CHANGE,
|
||||||
monitor: NO_CHANGE,
|
monitor: NO_CHANGE,
|
||||||
qualityProfileId: NO_CHANGE,
|
qualityProfileId: NO_CHANGE,
|
||||||
minimumAvailability: NO_CHANGE,
|
|
||||||
rootFolderPath: NO_CHANGE,
|
rootFolderPath: NO_CHANGE,
|
||||||
searchOnAdd: NO_CHANGE
|
minimumAvailability: NO_CHANGE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,12 +63,11 @@ class CollectionFooter extends Component {
|
|||||||
|
|
||||||
onUpdateSelectedPress = () => {
|
onUpdateSelectedPress = () => {
|
||||||
const {
|
const {
|
||||||
monitored,
|
|
||||||
monitor,
|
monitor,
|
||||||
|
monitored,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
rootFolderPath,
|
rootFolderPath
|
||||||
searchOnAdd
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const changes = {};
|
const changes = {};
|
||||||
@@ -138,10 +92,6 @@ class CollectionFooter extends Component {
|
|||||||
changes.rootFolderPath = rootFolderPath;
|
changes.rootFolderPath = rootFolderPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchOnAdd !== NO_CHANGE) {
|
|
||||||
changes.searchOnAdd = searchOnAdd === 'yes';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onUpdateSelectedPress(changes);
|
this.props.onUpdateSelectedPress(changes);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,10 +109,15 @@ class CollectionFooter extends Component {
|
|||||||
monitor,
|
monitor,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
rootFolderPath,
|
rootFolderPath
|
||||||
searchOnAdd
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const monitoredOptions = [
|
||||||
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
|
];
|
||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -170,7 +125,7 @@ class CollectionFooter extends Component {
|
|||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<CollectionFooterLabel
|
<CollectionFooterLabel
|
||||||
label={translate('MonitorCollection')}
|
label={translate('MonitorCollection')}
|
||||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
isSaving={isSaving}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -185,7 +140,7 @@ class CollectionFooter extends Component {
|
|||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<CollectionFooterLabel
|
<CollectionFooterLabel
|
||||||
label={translate('MonitorMovies')}
|
label={translate('MonitorMovies')}
|
||||||
isSaving={isSaving && monitor !== NO_CHANGE}
|
isSaving={isSaving}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -243,25 +198,10 @@ class CollectionFooter extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('SearchMoviesOnAdd')}
|
|
||||||
isSaving={isSaving && searchOnAdd !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
name="searchOnAdd"
|
|
||||||
value={searchOnAdd}
|
|
||||||
values={searchOnAddOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<div className={styles.buttonContainerContent}>
|
<div className={styles.buttonContainerContent}>
|
||||||
<CollectionFooterLabel
|
<CollectionFooterLabel
|
||||||
label={translate('CountCollectionsSelected', { count: selectedCount })}
|
label={translate('CollectionsSelectedInterp', [selectedCount])}
|
||||||
isSaving={false}
|
isSaving={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -115,16 +115,3 @@ $hoverScale: 1.05;
|
|||||||
color: var(--iconButtonHoverLightColor);
|
color: var(--iconButtonHoverLightColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.excluded {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-width: 0 25px 25px 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent var(--dangerColor) transparent transparent;
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ interface CssExports {
|
|||||||
'content': string;
|
'content': string;
|
||||||
'controls': string;
|
'controls': string;
|
||||||
'editorSelect': string;
|
'editorSelect': string;
|
||||||
'excluded': string;
|
|
||||||
'externalLinks': string;
|
'externalLinks': string;
|
||||||
'link': string;
|
'link': string;
|
||||||
'monitorToggleButton': string;
|
'monitorToggleButton': string;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user