mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d661242a | |||
| 324dac8db3 | |||
| bba69d8b22 | |||
| 1366f6e8b4 | |||
| f79712951b | |||
| 101b046753 | |||
| cd713e7252 | |||
| a54f54eb6e | |||
| f2af7a1b72 | |||
| a5b48153a6 | |||
| 1804e486d6 | |||
| b490177a77 | |||
| 7a90b4a6b2 | |||
| 558043f1b2 | |||
| 1423ad6aa4 | |||
| 087f9e12aa | |||
| c63d08e7a0 | |||
| 85b310c81c | |||
| 3c737c2c17 | |||
| 8ee70288c9 | |||
| 588e87e4be | |||
| 792b8182b2 | |||
| 4cec41324b | |||
| 10bb270da8 | |||
| b5e6a36878 | |||
| 126a5b118e | |||
| 0f1cf21c39 | |||
| 92a19a1a81 | |||
| 54965cfa6f | |||
| 14f27cf2b6 | |||
| a607f167f4 | |||
| 29449e83f9 | |||
| bb4e185644 | |||
| 085b1db77f | |||
| 7bdb3e437d | |||
| fcb0d8a930 | |||
| 7dc64c595c | |||
| 9a2b4bc81d | |||
| f228841dc7 | |||
| 02be9cf825 | |||
| 8809c207bb | |||
| 1be2cded74 | |||
| 0a189d00ef | |||
| 5fc63ecb3f | |||
| 3a74393d05 | |||
| 4cbf5cfc57 | |||
| 797142d6f3 | |||
| 2a472c50c1 | |||
| a12ff68fbd | |||
| 194926c7dd | |||
| 7dee5bb689 | |||
| 9b24dab71b | |||
| 62e1c02fe2 | |||
| 99b3d61862 | |||
| bd905567de | |||
| a8eea20d69 | |||
| 69ad0caf40 | |||
| 8a5c0ffd18 | |||
| c8b409ed0b | |||
| c5bcb13f63 | |||
| 80de711654 | |||
| 3fb558411e | |||
| 98384ab390 | |||
| 0c654377f4 | |||
| e8c925274a | |||
| 320bfeec16 | |||
| 638f92495c | |||
| 077b041d3f | |||
| ff3dd3ae42 | |||
| 2e3beddcbc | |||
| dc068bbf3d | |||
| 7a303c1ebf | |||
| 152f50a1ef | |||
| 9798202589 | |||
| 7969776339 | |||
| 288982d7bd | |||
| d39a3ade5b | |||
| 1fc6e88bc4 | |||
| e8e1841e6c | |||
| d17eb4f33f | |||
| 685f462959 | |||
| 7be8a34130 | |||
| 886711b496 | |||
| 5185e037da | |||
| 38e7e37d57 | |||
| 190c4c5893 | |||
| 0ec18ce4b3 | |||
| a08575b7bc | |||
| 556cc885ec | |||
| 586c0c6e13 | |||
| cec569461d | |||
| 8b79b5afbf | |||
| cd4552ce6f | |||
| 256439304b | |||
| bb44fbc362 | |||
| cd401f72f5 | |||
| c9624e7550 | |||
| 649702eaca | |||
| 1c52f0f5bd | |||
| dff85dc1f3 | |||
| 1090aeff75 | |||
| 086a0addba | |||
| 8b6cf34ce4 | |||
| 7f03a916f1 | |||
| 3a6d603a9e | |||
| cd2c7dc7fb | |||
| f1d76c3483 | |||
| 39eac4b5ad | |||
| 71e1003358 | |||
| 89b6a5d51f | |||
| 711637c448 | |||
| 2677d25980 | |||
| 56639bcd42 | |||
| 1ed62b9ced | |||
| a596dda253 | |||
| c0b354039d | |||
| 3b5078d117 | |||
| db1fee8d8a | |||
| 0d0575f3a9 | |||
| 2d82347a66 | |||
| 25838df550 | |||
| b3a8b99f9a | |||
| 93a852841f | |||
| ead1ec43be | |||
| 04b6dd44cb | |||
| 3db78079f3 | |||
| c8a6b9f565 | |||
| 811cafd9ae | |||
| ac7039d651 | |||
| a2d11cf684 | |||
| cc32635f6f | |||
| 10f9cb64ac | |||
| f77e27bace | |||
| 8ea6d59d59 | |||
| 98668d0d25 | |||
| 649d57a234 | |||
| dc7c8bf800 | |||
| 8d90c7678f | |||
| 02518e2116 | |||
| 3191a883dc | |||
| 31a714e6b3 | |||
| f7ca0b8b06 | |||
| 56be9502af | |||
| 77381d3f72 | |||
| 198e6324e0 | |||
| 81c9537e5a | |||
| d3cbb9be8d | |||
| 2e043c0cf7 | |||
| ada33dc065 | |||
| badb68b817 | |||
| 3bd1b3e972 | |||
| 6851de42a7 | |||
| dd0b7c91f9 | |||
| 45ac69e2d9 | |||
| 9ccf0ecdb1 | |||
| 48a3467572 | |||
| d0a10379f9 | |||
| caab5e3614 | |||
| 4e47695f89 | |||
| 83bd4d0686 | |||
| a75619c8ef | |||
| 28689006fb | |||
| 43b0589bea | |||
| c4aad5800c | |||
| 0c998dac5c | |||
| d41c0f0ab7 | |||
| 85b13b7e41 | |||
| 2a545a84b4 | |||
| 280083f4d7 | |||
| d6dcae3d6a | |||
| ebde4d3bc8 | |||
| 1ee30290ef | |||
| d303eae7c6 | |||
| 584910514a | |||
| a253181d7d |
@@ -0,0 +1,13 @@
|
|||||||
|
// 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": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
||||||
@@ -126,6 +126,7 @@ coverage*.xml
|
|||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
# VS outout folders
|
# VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+13
-18
@@ -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.4.3'
|
majorVersion: '5.8.3'
|
||||||
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.417'
|
dotnetVersion: '6.0.424'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-11'
|
macImage: 'macOS-12'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -166,10 +166,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(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: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(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@1
|
- task: SonarCloudPrepare@2
|
||||||
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@1
|
- task: SonarCloudAnalyze@2
|
||||||
|
|
||||||
- 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@1
|
- task: SonarCloudPrepare@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
@@ -1223,21 +1223,16 @@ 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@1
|
- task: SonarCloudAnalyze@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@4
|
- task: reportgenerator@5
|
||||||
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'
|
||||||
- task: PublishCodeCoverageResults@1
|
publishCodeCoverageResults: true
|
||||||
displayName: Publish Coverage Report
|
|
||||||
inputs:
|
|
||||||
codeCoverageTool: 'cobertura'
|
|
||||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
|
||||||
reportDirectory: './CoverageResults/combined/'
|
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
|
|||||||
@@ -21,15 +21,21 @@ 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.5.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
|
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
@@ -20,6 +21,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||||
|
|
||||||
class Blocklist extends Component {
|
class Blocklist extends Component {
|
||||||
@@ -114,9 +116,13 @@ class Blocklist extends Component {
|
|||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isClearingBlocklistExecuting,
|
isClearingBlocklistExecuting,
|
||||||
|
onFilterSelect,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -161,6 +167,15 @@ class Blocklist extends Component {
|
|||||||
iconName={icons.TABLE}
|
iconName={icons.TABLE}
|
||||||
/>
|
/>
|
||||||
</TableOptionsModalWrapper>
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={BlocklistFilterModal}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
@@ -180,7 +195,11 @@ class Blocklist extends Component {
|
|||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('NoHistoryBlocklist')}
|
{
|
||||||
|
selectedFilterKey === 'all' ?
|
||||||
|
translate('NoHistoryBlocklist') :
|
||||||
|
translate('BlocklistFilterHasNoItems')
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,11 +270,15 @@ Blocklist.propTypes = {
|
|||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||||
onRemoveSelected: PropTypes.func.isRequired,
|
onRemoveSelected: PropTypes.func.isRequired,
|
||||||
onClearBlocklistPress: PropTypes.func.isRequired
|
onClearBlocklistPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Blocklist;
|
export default Blocklist;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
|
|||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import Blocklist from './Blocklist';
|
import Blocklist from './Blocklist';
|
||||||
@@ -13,10 +14,12 @@ import Blocklist from './Blocklist';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.blocklist,
|
(state) => state.blocklist,
|
||||||
|
createCustomFiltersSelector('blocklist'),
|
||||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||||
(blocklist, isClearingBlocklistExecuting) => {
|
(blocklist, customFilters, isClearingBlocklistExecuting) => {
|
||||||
return {
|
return {
|
||||||
isClearingBlocklistExecuting,
|
isClearingBlocklistExecuting,
|
||||||
|
customFilters,
|
||||||
...blocklist
|
...blocklist
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -97,6 +100,14 @@ class BlocklistConnector extends Component {
|
|||||||
this.props.setBlocklistSort({ sortKey });
|
this.props.setBlocklistSort({ sortKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setBlocklistFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onClearBlocklistPress = () => {
|
||||||
|
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||||
|
};
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
onTableOptionChange = (payload) => {
|
||||||
this.props.setBlocklistTableOption(payload);
|
this.props.setBlocklistTableOption(payload);
|
||||||
|
|
||||||
@@ -105,10 +116,6 @@ class BlocklistConnector extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClearBlocklistPress = () => {
|
|
||||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -122,6 +129,7 @@ class BlocklistConnector extends Component {
|
|||||||
onPageSelect={this.onPageSelect}
|
onPageSelect={this.onPageSelect}
|
||||||
onRemoveSelected={this.onRemoveSelected}
|
onRemoveSelected={this.onRemoveSelected}
|
||||||
onSortPress={this.onSortPress}
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
@@ -142,6 +150,7 @@ BlocklistConnector.propTypes = {
|
|||||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||||
removeBlocklistItems: PropTypes.func.isRequired,
|
removeBlocklistItems: PropTypes.func.isRequired,
|
||||||
setBlocklistSort: PropTypes.func.isRequired,
|
setBlocklistSort: PropTypes.func.isRequired,
|
||||||
|
setBlocklistFilter: PropTypes.func.isRequired,
|
||||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||||
clearBlocklist: PropTypes.func.isRequired,
|
clearBlocklist: PropTypes.func.isRequired,
|
||||||
executeCommand: PropTypes.func.isRequired
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -219,6 +219,7 @@ class Queue extends Component {
|
|||||||
>
|
>
|
||||||
<TableOptionsModalWrapper
|
<TableOptionsModalWrapper
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
maxPageSize={200}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
optionsComponent={QueueOptionsConnector}
|
optionsComponent={QueueOptionsConnector}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ function QueueStatus(props) {
|
|||||||
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;
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
|||||||
{
|
{
|
||||||
key: 'blocklistAndSearch',
|
key: 'blocklistAndSearch',
|
||||||
value: translate('BlocklistAndSearch'),
|
value: translate('BlocklistAndSearch'),
|
||||||
|
isDisabled: isPending,
|
||||||
hint: multipleSelected
|
hint: multipleSelected
|
||||||
? translate('BlocklistAndSearchMultipleHint')
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
: translate('BlocklistAndSearchHint'),
|
: translate('BlocklistAndSearchHint'),
|
||||||
@@ -130,7 +131,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
|||||||
: translate('BlocklistOnlyHint'),
|
: translate('BlocklistOnlyHint'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [multipleSelected]);
|
}, [isPending, multipleSelected]);
|
||||||
|
|
||||||
const handleRemovalMethodChange = useCallback(
|
const handleRemovalMethodChange = useCallback(
|
||||||
({ value }: { value: RemovalMethod }) => {
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ 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>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ 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 {
|
||||||
@@ -121,6 +123,20 @@ function AppRoutes(props) {
|
|||||||
component={BlocklistConnector}
|
component={BlocklistConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Wanted
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/missing"
|
||||||
|
component={MissingConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/cutoffunmet"
|
||||||
|
component={CutoffUnmetConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Settings
|
Settings
|
||||||
*/}
|
*/}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
|
||||||
|
interface ApplyThemeProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createThemeSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
|
||||||
|
(theme) => {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||||
|
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 <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplyTheme;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
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';
|
||||||
@@ -54,6 +55,7 @@ export interface AppSectionState {
|
|||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import Blocklist from 'typings/Blocklist';
|
||||||
|
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
|
||||||
|
|
||||||
|
interface BlocklistAppState
|
||||||
|
extends AppSectionState<Blocklist>,
|
||||||
|
AppSectionFilterState<Blocklist> {}
|
||||||
|
|
||||||
|
export default BlocklistAppState;
|
||||||
@@ -25,6 +25,7 @@ export interface MovieIndexAppState {
|
|||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export interface MovieIndexAppState {
|
|||||||
showAdded: boolean;
|
showAdded: boolean;
|
||||||
showPath: boolean;
|
showPath: boolean;
|
||||||
showSizeOnDisk: boolean;
|
showSizeOnDisk: boolean;
|
||||||
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -115,3 +115,16 @@ $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,6 +6,7 @@ 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;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
||||||
import styles from './CollectionMovie.css';
|
import styles from './CollectionMovie.css';
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ class CollectionMovie extends Component {
|
|||||||
isAvailable,
|
isAvailable,
|
||||||
movieFile,
|
movieFile,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
|
isExcluded,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
posterHeight,
|
posterHeight,
|
||||||
detailedProgressBar,
|
detailedProgressBar,
|
||||||
@@ -107,6 +109,15 @@ class CollectionMovie extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isExcluded ?
|
||||||
|
<div
|
||||||
|
className={styles.excluded}
|
||||||
|
title={translate('Excluded')}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
style={elementStyle}
|
style={elementStyle}
|
||||||
@@ -189,6 +200,7 @@ CollectionMovie.propTypes = {
|
|||||||
posterHeight: PropTypes.number.isRequired,
|
posterHeight: PropTypes.number.isRequired,
|
||||||
detailedProgressBar: PropTypes.bool.isRequired,
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
isExistingMovie: PropTypes.bool,
|
isExistingMovie: PropTypes.bool,
|
||||||
|
isExcluded: PropTypes.bool,
|
||||||
tmdbId: PropTypes.number.isRequired,
|
tmdbId: PropTypes.number.isRequired,
|
||||||
imdbId: PropTypes.string,
|
imdbId: PropTypes.string,
|
||||||
youTubeTrailerId: PropTypes.string,
|
youTubeTrailerId: PropTypes.string,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
@@ -228,7 +229,7 @@ class FilterBuilderRow extends Component {
|
|||||||
key: name,
|
key: name,
|
||||||
value: typeof label === 'function' ? label() : label
|
value: typeof label === 'function' ? label() : label
|
||||||
};
|
};
|
||||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
}).sort(sortByProp('value'));
|
||||||
|
|
||||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { filterBuilderTypes } from 'Helpers/Props';
|
import { filterBuilderTypes } from 'Helpers/Props';
|
||||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
function createTagListSelector() {
|
function createTagListSelector() {
|
||||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []).sort(sortByName);
|
}, []).sort(sortByProp('name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.uniqBy(items, 'id');
|
return _.uniqBy(items, 'id');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
|||||||
|
|
||||||
const tagList = allMovies
|
const tagList = allMovies
|
||||||
.map((movie) => ({ id: movie.id, name: movie.title }))
|
.map((movie) => ({ id: movie.id, name: movie.title }))
|
||||||
.sort(sortByName);
|
.sort(sortByProp('name'));
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CustomFilter from './CustomFilter';
|
import CustomFilter from './CustomFilter';
|
||||||
import styles from './CustomFiltersModalContent.css';
|
import styles from './CustomFiltersModalContent.css';
|
||||||
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||||
.map((customFilter) => {
|
.map((customFilter) => {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -22,7 +23,7 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
||||||
|
|
||||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
||||||
return {
|
return {
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name,
|
value: downloadClient.name,
|
||||||
@@ -33,7 +34,7 @@ function createMapStateToProps() {
|
|||||||
if (includeAny) {
|
if (includeAny) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 0,
|
key: 0,
|
||||||
value: '(Any)'
|
value: `(${translate('Any')})`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,26 +271,32 @@ class EnhancedSelectInput extends Component {
|
|||||||
this.setState({ isOpen: !this.state.isOpen });
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelect = (value) => {
|
onSelect = (newValue) => {
|
||||||
if (Array.isArray(this.props.value)) {
|
const { name, value, values, onChange } = this.props;
|
||||||
let newValue = null;
|
const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
|
||||||
const index = this.props.value.indexOf(value);
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
let arrayValue = null;
|
||||||
|
const index = value.indexOf(newValue);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
||||||
} else {
|
} else {
|
||||||
newValue = [...this.props.value];
|
arrayValue = [...value];
|
||||||
newValue.splice(index, 1);
|
arrayValue.splice(index, 1);
|
||||||
}
|
}
|
||||||
this.props.onChange({
|
onChange({
|
||||||
name: this.props.name,
|
name,
|
||||||
value: newValue
|
value: arrayValue,
|
||||||
|
additionalProperties
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ isOpen: false });
|
this.setState({ isOpen: false });
|
||||||
|
|
||||||
this.props.onChange({
|
onChange({
|
||||||
name: this.props.name,
|
name,
|
||||||
value
|
value: newValue,
|
||||||
|
additionalProperties
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -485,7 +491,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
values.map((v, index) => {
|
values.map((v, index) => {
|
||||||
const hasParent = v.parentKey !== undefined;
|
const hasParent = v.parentKey !== undefined;
|
||||||
const depth = hasParent ? 1 : 0;
|
const depth = hasParent ? 1 : 0;
|
||||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
||||||
return (
|
return (
|
||||||
<OptionComponent
|
<OptionComponent
|
||||||
key={v.key}
|
key={v.key}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import EnhancedSelectInput from './EnhancedSelectInput';
|
|||||||
const importantFieldNames = [
|
const importantFieldNames = [
|
||||||
'baseUrl',
|
'baseUrl',
|
||||||
'apiPath',
|
'apiPath',
|
||||||
'apiKey'
|
'apiKey',
|
||||||
|
'authToken'
|
||||||
];
|
];
|
||||||
|
|
||||||
function getProviderDataKey(providerData) {
|
function getProviderDataKey(providerData) {
|
||||||
@@ -34,7 +35,9 @@ function getSelectOptions(items) {
|
|||||||
key: option.value,
|
key: option.value,
|
||||||
value: option.name,
|
value: option.name,
|
||||||
hint: option.hint,
|
hint: option.hint,
|
||||||
parentKey: option.parentValue
|
parentKey: option.parentValue,
|
||||||
|
isDisabled: option.isDisabled,
|
||||||
|
additionalProperties: option.additionalProperties
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,7 +150,7 @@ EnhancedSelectInputConnector.propTypes = {
|
|||||||
provider: PropTypes.string.isRequired,
|
provider: PropTypes.string.isRequired,
|
||||||
providerData: PropTypes.object.isRequired,
|
providerData: PropTypes.object.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectOptionsProviderAction: PropTypes.string,
|
selectOptionsProviderAction: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
|||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||||
|
import MovieTagInput from './MovieTagInput';
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
import PasswordInput from './PasswordInput';
|
import PasswordInput from './PasswordInput';
|
||||||
@@ -89,6 +90,10 @@ function getComponent(type) {
|
|||||||
|
|
||||||
case inputTypes.DYNAMIC_SELECT:
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
return EnhancedSelectInputConnector;
|
return EnhancedSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.MOVIE_TAG:
|
||||||
|
return MovieTagInput;
|
||||||
|
|
||||||
case inputTypes.TAG:
|
case inputTypes.TAG:
|
||||||
return TagInputConnector;
|
return TagInputConnector;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
|||||||
items
|
items
|
||||||
} = indexers;
|
} = indexers;
|
||||||
|
|
||||||
const values = items.sort(sortByName).map((indexer) => ({
|
const values = items.sort(sortByProp('name')).map((indexer) => ({
|
||||||
key: indexer.id,
|
key: indexer.id,
|
||||||
value: indexer.name
|
value: indexer.name
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import TagInputConnector from './TagInputConnector';
|
||||||
|
|
||||||
|
interface MovieTagInputProps {
|
||||||
|
name: string;
|
||||||
|
value: number | number[];
|
||||||
|
onChange: ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
value: number | number[];
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MovieTagInput(props: MovieTagInputProps) {
|
||||||
|
const { value, onChange, ...otherProps } = props;
|
||||||
|
const isArray = Array.isArray(value);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||||
|
if (isArray) {
|
||||||
|
onChange({ name, value: newValue });
|
||||||
|
} else {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isArray, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
let finalValue: number[] = [];
|
||||||
|
|
||||||
|
if (isArray) {
|
||||||
|
finalValue = value;
|
||||||
|
} else if (value === 0) {
|
||||||
|
finalValue = [];
|
||||||
|
} else {
|
||||||
|
finalValue = [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||||
|
<TagInputConnector
|
||||||
|
{...otherProps}
|
||||||
|
value={finalValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.input {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
font-family: $passwordFamily;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './PasswordInput.css';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
function onCopy(e) {
|
function onCopy(e) {
|
||||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
{...props}
|
{...props}
|
||||||
|
type="password"
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
className: PropTypes.string.isRequired
|
...TextInput.props
|
||||||
};
|
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
|
||||||
className: styles.input
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordInput;
|
export default PasswordInput;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||||||
return inputTypes.DYNAMIC_SELECT;
|
return inputTypes.DYNAMIC_SELECT;
|
||||||
}
|
}
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
|
case 'movieTag':
|
||||||
|
return inputTypes.MOVIE_TAG;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
return inputTypes.TEXT_TAG;
|
return inputTypes.TEXT_TAG;
|
||||||
case 'tagSelect':
|
case 'tagSelect':
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterMenuItem from './FilterMenuItem';
|
import FilterMenuItem from './FilterMenuItem';
|
||||||
import MenuContent from './MenuContent';
|
import MenuContent from './MenuContent';
|
||||||
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort(sortByProp('label'))
|
||||||
.map((filter) => {
|
.map((filter) => {
|
||||||
return (
|
return (
|
||||||
<FilterMenuItem
|
<FilterMenuItem
|
||||||
|
|||||||
@@ -71,6 +71,22 @@ const links = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
iconName: icons.WARNING,
|
||||||
|
title: () => translate('Wanted'),
|
||||||
|
to: '/wanted/missing',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: () => translate('Missing'),
|
||||||
|
to: '/wanted/missing'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: () => translate('CutoffUnmet'),
|
||||||
|
to: '/wanted/cutoffunmet'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SETTINGS,
|
iconName: icons.SETTINGS,
|
||||||
title: () => translate('Settings'),
|
title: () => translate('Settings'),
|
||||||
|
|||||||
@@ -244,6 +244,26 @@ class SignalRConnector extends Component {
|
|||||||
this.props.dispatchSetVersion({ version });
|
this.props.dispatchSetVersion({ version });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleWantedCutoff = (body) => {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({
|
||||||
|
section: 'wanted.cutoffUnmet',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleWantedMissing = (body) => {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({
|
||||||
|
section: 'wanted.missing',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleSystemTask = () => {
|
handleSystemTask = () => {
|
||||||
this.props.dispatchFetchCommands();
|
this.props.dispatchFetchCommands();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,11 +49,12 @@ class TableOptionsModal extends Component {
|
|||||||
|
|
||||||
onPageSizeChange = ({ value }) => {
|
onPageSizeChange = ({ value }) => {
|
||||||
let pageSizeError = null;
|
let pageSizeError = null;
|
||||||
|
const maxPageSize = this.props.maxPageSize ?? 250;
|
||||||
|
|
||||||
if (value < 5) {
|
if (value < 5) {
|
||||||
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
|
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
|
||||||
} else if (value > 250) {
|
} else if (value > maxPageSize) {
|
||||||
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' });
|
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: `${maxPageSize}` });
|
||||||
} else {
|
} else {
|
||||||
this.props.onTableOptionChange({ pageSize: value });
|
this.props.onTableOptionChange({ pageSize: value });
|
||||||
}
|
}
|
||||||
@@ -248,6 +249,7 @@ TableOptionsModal.propTypes = {
|
|||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
pageSize: PropTypes.number,
|
pageSize: PropTypes.number,
|
||||||
|
maxPageSize: PropTypes.number,
|
||||||
canModifyColumns: PropTypes.bool.isRequired,
|
canModifyColumns: PropTypes.bool.isRequired,
|
||||||
optionsComponent: PropTypes.elementType,
|
optionsComponent: PropTypes.elementType,
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
import styles from './TagList.css';
|
import styles from './TagList.css';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ function TagList({ tags, tagList }) {
|
|||||||
const sortedTags = tags
|
const sortedTags = tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((tag) => !!tag)
|
.filter((tag) => !!tag)
|
||||||
.sort((a, b) => a.label.localeCompare(b.label));
|
.sort(sortByProp('label'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
|
|||||||
@@ -25,14 +25,3 @@
|
|||||||
font-family: 'Ubuntu Mono';
|
font-family: 'Ubuntu Mono';
|
||||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* text-security-disc
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-family: 'text-security-disc';
|
|
||||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -15,5 +15,5 @@
|
|||||||
"start_url": "../../../../",
|
"start_url": "../../../../",
|
||||||
"theme_color": "#3a3f51",
|
"theme_color": "#3a3f51",
|
||||||
"background_color": "#3a3f51",
|
"background_color": "#3a3f51",
|
||||||
"display": "minimal-ui"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,9 +75,19 @@ class DiscoverMovie extends Component {
|
|||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection
|
sortDirection,
|
||||||
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
if (includeRecommendations !== prevProps.includeRecommendations ||
|
||||||
|
includeTrending !== prevProps.includeTrending ||
|
||||||
|
includePopular !== prevProps.includePopular
|
||||||
|
) {
|
||||||
|
this.props.dispatchFetchListMovies();
|
||||||
|
}
|
||||||
|
|
||||||
if (sortKey !== prevProps.sortKey ||
|
if (sortKey !== prevProps.sortKey ||
|
||||||
sortDirection !== prevProps.sortDirection ||
|
sortDirection !== prevProps.sortDirection ||
|
||||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||||
@@ -443,6 +453,9 @@ DiscoverMovie.propTypes = {
|
|||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
view: PropTypes.string.isRequired,
|
view: PropTypes.string.isRequired,
|
||||||
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
isSyncingLists: PropTypes.bool.isRequired,
|
isSyncingLists: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
onSortSelect: PropTypes.func.isRequired,
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
@@ -451,7 +464,8 @@ DiscoverMovie.propTypes = {
|
|||||||
onScroll: PropTypes.func.isRequired,
|
onScroll: PropTypes.func.isRequired,
|
||||||
onAddMoviesPress: PropTypes.func.isRequired,
|
onAddMoviesPress: PropTypes.func.isRequired,
|
||||||
onExcludeMoviesPress: PropTypes.func.isRequired,
|
onExcludeMoviesPress: PropTypes.func.isRequired,
|
||||||
onImportListSyncPress: PropTypes.func.isRequired
|
onImportListSyncPress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchListMovies: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DiscoverMovie;
|
export default DiscoverMovie;
|
||||||
|
|||||||
@@ -17,15 +17,18 @@ import DiscoverMovie from './DiscoverMovie';
|
|||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
(state) => state.discoverMovie,
|
||||||
createDiscoverMovieClientSideCollectionItemsSelector('discoverMovie'),
|
createDiscoverMovieClientSideCollectionItemsSelector('discoverMovie'),
|
||||||
createCommandExecutingSelector(commandNames.IMPORT_LIST_SYNC),
|
createCommandExecutingSelector(commandNames.IMPORT_LIST_SYNC),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(
|
(
|
||||||
|
discoverMovie,
|
||||||
movies,
|
movies,
|
||||||
isSyncingLists,
|
isSyncingLists,
|
||||||
dimensionsState
|
dimensionsState
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
|
...discoverMovie.options,
|
||||||
...movies,
|
...movies,
|
||||||
isSyncingLists,
|
isSyncingLists,
|
||||||
isSmallScreen: dimensionsState.isSmallScreen
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
|
|||||||
+45
-4
@@ -49,7 +49,9 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
|||||||
showRatings: props.showRatings,
|
showRatings: props.showRatings,
|
||||||
showYear: props.showYear,
|
showYear: props.showYear,
|
||||||
showGenres: props.showGenres,
|
showGenres: props.showGenres,
|
||||||
includeRecommendations: props.includeRecommendations
|
includeRecommendations: props.includeRecommendations,
|
||||||
|
includeTrending: props.includeTrending,
|
||||||
|
includePopular: props.includePopular
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +63,9 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
|||||||
showRatings,
|
showRatings,
|
||||||
showCertification,
|
showCertification,
|
||||||
showGenres,
|
showGenres,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const state = {};
|
const state = {};
|
||||||
@@ -94,6 +98,14 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
|||||||
state.includeRecommendations = includeRecommendations;
|
state.includeRecommendations = includeRecommendations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeTrending !== prevProps.includeTrending) {
|
||||||
|
state.includeTrending = includeTrending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePopular !== prevProps.includePopular) {
|
||||||
|
state.includePopular = includePopular;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(state)) {
|
if (!_.isEmpty(state)) {
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
@@ -135,19 +147,22 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
|||||||
showRatings,
|
showRatings,
|
||||||
showYear,
|
showYear,
|
||||||
showGenres,
|
showGenres,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Overview Options
|
{translate('OverviewOptions')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="includeRecommendations"
|
name="includeRecommendations"
|
||||||
@@ -157,6 +172,30 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeTrending')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeTrending"
|
||||||
|
value={includeTrending}
|
||||||
|
helpText={translate('IncludeTrendingMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludePopular')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includePopular"
|
||||||
|
value={includePopular}
|
||||||
|
helpText={translate('IncludePopularMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
@@ -246,6 +285,8 @@ DiscoverMovieOverviewOptionsModalContent.propTypes = {
|
|||||||
showCertification: PropTypes.bool.isRequired,
|
showCertification: PropTypes.bool.isRequired,
|
||||||
showGenres: PropTypes.bool.isRequired,
|
showGenres: PropTypes.bool.isRequired,
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
onChangeOverviewOption: PropTypes.func.isRequired,
|
onChangeOverviewOption: PropTypes.func.isRequired,
|
||||||
onChangeOption: PropTypes.func.isRequired,
|
onChangeOption: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|||||||
+44
-4
@@ -45,7 +45,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
size: props.size,
|
size: props.size,
|
||||||
showTitle: props.showTitle,
|
showTitle: props.showTitle,
|
||||||
includeRecommendations: props.includeRecommendations
|
includeRecommendations: props.includeRecommendations,
|
||||||
|
includeTrending: props.includeTrending,
|
||||||
|
includePopular: props.includePopular
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
showTitle,
|
showTitle,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const state = {};
|
const state = {};
|
||||||
@@ -70,6 +74,14 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
state.includeRecommendations = includeRecommendations;
|
state.includeRecommendations = includeRecommendations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeTrending !== prevProps.includeTrending) {
|
||||||
|
state.includeTrending = includeTrending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePopular !== prevProps.includePopular) {
|
||||||
|
state.includePopular = includePopular;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(state)) {
|
if (!_.isEmpty(state)) {
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
@@ -107,13 +119,15 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
showTitle,
|
showTitle,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Poster Options
|
{translate('PosterOptions')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -130,6 +144,30 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeTrending')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeTrending"
|
||||||
|
value={includeTrending}
|
||||||
|
helpText={translate('IncludeTrendingMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludePopular')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includePopular"
|
||||||
|
value={includePopular}
|
||||||
|
helpText={translate('IncludePopularMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
@@ -172,6 +210,8 @@ DiscoverMoviePosterOptionsModalContent.propTypes = {
|
|||||||
size: PropTypes.string.isRequired,
|
size: PropTypes.string.isRequired,
|
||||||
showTitle: PropTypes.bool.isRequired,
|
showTitle: PropTypes.bool.isRequired,
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
onChangePosterOption: PropTypes.func.isRequired,
|
onChangePosterOption: PropTypes.func.isRequired,
|
||||||
onChangeOption: PropTypes.func.isRequired,
|
onChangeOption: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -15,17 +15,29 @@ class DiscoverMovieTableOptions extends Component {
|
|||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
includeRecommendations: props.includeRecommendations
|
includeRecommendations: props.includeRecommendations,
|
||||||
|
includeTrending: props.includeTrending,
|
||||||
|
includePopular: props.includePopular
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { includeRecommendations } = this.props;
|
const {
|
||||||
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||||
this.setState({
|
this.setState({ includeRecommendations });
|
||||||
includeRecommendations
|
}
|
||||||
});
|
|
||||||
|
if (includeTrending !== prevProps.includeTrending) {
|
||||||
|
this.setState({ includeTrending });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePopular !== prevProps.includePopular) {
|
||||||
|
this.setState({ includePopular });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,27 +59,57 @@ class DiscoverMovieTableOptions extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<>
|
||||||
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="includeRecommendations"
|
name="includeRecommendations"
|
||||||
value={includeRecommendations}
|
value={includeRecommendations}
|
||||||
helpText={translate('IncludeRecommendationsHelpText')}
|
helpText={translate('IncludeRecommendationsHelpText')}
|
||||||
onChange={this.onChangeOption}
|
onChange={this.onChangeOption}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeTrending')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeTrending"
|
||||||
|
value={includeTrending}
|
||||||
|
helpText={translate('IncludeTrendingMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludePopular')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includePopular"
|
||||||
|
value={includePopular}
|
||||||
|
helpText={translate('IncludePopularMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscoverMovieTableOptions.propTypes = {
|
DiscoverMovieTableOptions.propTypes = {
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
onChangeOption: PropTypes.func.isRequired
|
onChangeOption: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import styles from './ListMovieStatusCell.css';
|
import styles from './ListMovieStatusCell.css';
|
||||||
|
|
||||||
function ListMovieStatusCell(props) {
|
function ListMovieStatusCell(props) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
|||||||
export const LANGUAGE_SELECT = 'languageSelect';
|
export const LANGUAGE_SELECT = 'languageSelect';
|
||||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||||
export const SELECT = 'select';
|
export const SELECT = 'select';
|
||||||
|
export const MOVIE_TAG = 'movieTag';
|
||||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
export const TEXT = 'text';
|
export const TEXT = 'text';
|
||||||
@@ -45,6 +46,7 @@ export const all = [
|
|||||||
INDEXER_FLAGS_SELECT,
|
INDEXER_FLAGS_SELECT,
|
||||||
LANGUAGE_SELECT,
|
LANGUAGE_SELECT,
|
||||||
SELECT,
|
SELECT,
|
||||||
|
MOVIE_TAG,
|
||||||
DYNAMIC_SELECT,
|
DYNAMIC_SELECT,
|
||||||
TAG,
|
TAG,
|
||||||
TEXT,
|
TEXT,
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (allowMovieChange && movie && quality && languages) {
|
if (allowMovieChange && movie && quality && languages && size > 0) {
|
||||||
onSelectedChange({
|
onSelectedChange({
|
||||||
id,
|
id,
|
||||||
hasMovieFileId: !!movieFileId,
|
hasMovieFileId: !!movieFileId,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { scrollDirections } from 'Helpers/Props';
|
|||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectMovieModalTableHeader from './SelectMovieModalTableHeader';
|
import SelectMovieModalTableHeader from './SelectMovieModalTableHeader';
|
||||||
import SelectMovieRow from './SelectMovieRow';
|
import SelectMovieRow from './SelectMovieRow';
|
||||||
@@ -162,9 +163,7 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const sorted = [...allMovies].sort((a, b) =>
|
const sorted = [...allMovies].sort(sortByProp('sortTitle'));
|
||||||
a.sortTitle.localeCompare(b.sortTitle)
|
|
||||||
);
|
|
||||||
|
|
||||||
return sorted.filter(
|
return sorted.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
|
|||||||
@@ -6,7 +6,17 @@
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.folderPath {
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: var(--defaultFontFamily);
|
||||||
|
}
|
||||||
|
|
||||||
.deleteFilesMessage {
|
.deleteFilesMessage {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: var(--dangerColor);
|
color: var(--dangerColor);
|
||||||
|
|
||||||
|
.deleteCount {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: var(--warningColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'deleteCount': string;
|
||||||
'deleteFilesMessage': string;
|
'deleteFilesMessage': string;
|
||||||
|
'folderPath': string;
|
||||||
'pathContainer': string;
|
'pathContainer': string;
|
||||||
'pathIcon': string;
|
'pathIcon': string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
|||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
@@ -49,34 +50,26 @@ class DeleteMovieModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
path,
|
path,
|
||||||
hasFile,
|
statistics = {},
|
||||||
statistics,
|
|
||||||
deleteOptions,
|
deleteOptions,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onDeleteOptionChange
|
onDeleteOptionChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
movieFileCount = 0,
|
||||||
sizeOnDisk = 0
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
const addImportExclusion = deleteOptions.addImportExclusion;
|
const addImportExclusion = deleteOptions.addImportExclusion;
|
||||||
|
|
||||||
let deleteFilesLabel = hasFile ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [0]);
|
|
||||||
let deleteFilesHelpText = translate('DeleteFilesHelpText');
|
|
||||||
|
|
||||||
if (!hasFile) {
|
|
||||||
deleteFilesLabel = translate('DeleteMovieFolderLabel');
|
|
||||||
deleteFilesHelpText = translate('DeleteMovieFolderHelpText');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('DeleteHeader', [title])}
|
{translate('DeleteHeader', { title })}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -105,32 +98,32 @@ class DeleteMovieModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{deleteFilesLabel}</FormLabel>
|
<FormLabel>{movieFileCount === 0 ? translate('DeleteMovieFolder') : translate('DeleteMovieFiles', { movieFileCount })}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="deleteFiles"
|
name="deleteFiles"
|
||||||
value={deleteFiles}
|
value={deleteFiles}
|
||||||
helpText={deleteFilesHelpText}
|
helpText={movieFileCount === 0 ? translate('DeleteMovieFolderHelpText') : translate('DeleteMovieFilesHelpText')}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onChange={this.onDeleteFilesChange}
|
onChange={this.onDeleteFilesChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
deleteFiles &&
|
deleteFiles ?
|
||||||
<div className={styles.deleteFilesMessage}>
|
<div className={styles.deleteFilesMessage}>
|
||||||
<div>
|
<div><InlineMarkdown data={translate('DeleteMovieFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
|
||||||
{translate('DeleteTheMovieFolder', { path })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!!hasFile &&
|
movieFileCount ?
|
||||||
<div>
|
<div className={styles.deleteCount}>
|
||||||
{hasFile} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
|
{translate('DeleteMovieFolderMovieCount', { movieFileCount, size: formatBytes(sizeOnDisk) })}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
@@ -51,7 +50,8 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
|
|||||||
const lineHeight = parseFloat(fonts.lineHeight);
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
function getFanartUrl(images) {
|
function getFanartUrl(images) {
|
||||||
return _.find(images, { coverType: 'fanart' })?.url;
|
const image = images.find((img) => img.coverType === 'fanart');
|
||||||
|
return image?.url ?? image?.remoteUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MovieDetails extends Component {
|
class MovieDetails extends Component {
|
||||||
@@ -285,8 +285,10 @@ class MovieDetails extends Component {
|
|||||||
const fanartUrl = getFanartUrl(images);
|
const fanartUrl = getFanartUrl(images);
|
||||||
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
||||||
|
|
||||||
|
const pageTitle = `${title}${year > 0 ? ` (${year})` : ''}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title={title}>
|
<PageContent title={pageTitle}>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
@@ -368,7 +370,7 @@ class MovieDetails extends Component {
|
|||||||
<MoviePoster
|
<MoviePoster
|
||||||
className={styles.poster}
|
className={styles.poster}
|
||||||
images={images}
|
images={images}
|
||||||
size={250}
|
size={500}
|
||||||
lazy={false}
|
lazy={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,29 @@ import translate from 'Utilities/String/translate';
|
|||||||
import styles from './MovieReleaseDates.css';
|
import styles from './MovieReleaseDates.css';
|
||||||
|
|
||||||
interface MovieReleaseDatesProps {
|
interface MovieReleaseDatesProps {
|
||||||
inCinemas: string;
|
inCinemas?: string;
|
||||||
physicalRelease: string;
|
digitalRelease?: string;
|
||||||
digitalRelease: string;
|
physicalRelease?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||||
const { inCinemas, physicalRelease, digitalRelease } = props;
|
const { inCinemas, digitalRelease, physicalRelease } = props;
|
||||||
|
|
||||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||||
createUISettingsSelector()
|
createUISettingsSelector()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!inCinemas && !physicalRelease && !digitalRelease) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.dateIcon}>
|
||||||
|
<Icon name={icons.MISSING} />
|
||||||
|
</div>
|
||||||
|
{translate('NoMovieReleaseDatesAvailable')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{inCinemas ? (
|
{inCinemas ? (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import MovieTags from './MovieTags';
|
import MovieTags from './MovieTags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -12,8 +13,8 @@ function createMapStateToProps() {
|
|||||||
const tags = movie.tags
|
const tags = movie.tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((tag) => !!tag)
|
.filter((tag) => !!tag)
|
||||||
.map((tag) => tag.label)
|
.sort(sortByProp('label'))
|
||||||
.sort((a, b) => a.localeCompare(b));
|
.map((tag) => tag.label);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags
|
tags
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function MovieHistoryModal(props) {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
size={sizes.EXTRA_LARGE}
|
size={sizes.EXTRA_EXTRA_LARGE}
|
||||||
>
|
>
|
||||||
<MovieHistoryModalContentConnector
|
<MovieHistoryModalContentConnector
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
|
|||||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieHistoryRow.css';
|
import styles from './MovieHistoryRow.css';
|
||||||
@@ -109,9 +109,9 @@ class MovieHistoryRow extends Component {
|
|||||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<TableRowCell>
|
||||||
date={date}
|
{formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
/>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
<TableRowCell className={styles.actions}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -189,6 +189,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
|||||||
>
|
>
|
||||||
{translate('OriginalLanguage')}
|
{translate('OriginalLanguage')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="tags"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('Tags')}
|
||||||
|
</SortMenuItem>
|
||||||
</MenuContent>
|
</MenuContent>
|
||||||
</SortMenu>
|
</SortMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
|||||||
import MovieIndexFooter from './MovieIndexFooter';
|
import MovieIndexFooter from './MovieIndexFooter';
|
||||||
import MovieIndexRefreshMovieButton from './MovieIndexRefreshMovieButton';
|
import MovieIndexRefreshMovieButton from './MovieIndexRefreshMovieButton';
|
||||||
import MovieIndexSearchButton from './MovieIndexSearchButton';
|
import MovieIndexSearchButton from './MovieIndexSearchButton';
|
||||||
|
import MovieIndexSearchMenuItem from './MovieIndexSearchMenuItem';
|
||||||
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
||||||
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
||||||
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
||||||
@@ -247,6 +248,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
<MovieIndexSearchButton
|
<MovieIndexSearchButton
|
||||||
isSelectMode={isSelectMode}
|
isSelectMode={isSelectMode}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
overflowComponent={MovieIndexSearchMenuItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||||||
interface MovieIndexSearchButtonProps {
|
interface MovieIndexSearchButtonProps {
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
selectedFilterKey: string;
|
selectedFilterKey: string;
|
||||||
|
overflowComponent: React.FunctionComponent<never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useSelect } from 'App/SelectContext';
|
||||||
|
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||||
|
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
||||||
|
import { MOVIE_SEARCH } from 'Commands/commandNames';
|
||||||
|
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
||||||
|
interface MovieIndexSearchMenuItemProps {
|
||||||
|
isSelectMode: boolean;
|
||||||
|
selectedFilterKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexSearchMenuItem(props: MovieIndexSearchMenuItemProps) {
|
||||||
|
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
|
||||||
|
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { isSelectMode, selectedFilterKey } = props;
|
||||||
|
const [selectState] = useSelect();
|
||||||
|
const { selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedMovieIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const moviesToSearch =
|
||||||
|
isSelectMode && selectedMovieIds.length > 0
|
||||||
|
? selectedMovieIds
|
||||||
|
: items.map((m) => m.id);
|
||||||
|
|
||||||
|
const searchIndexLabel =
|
||||||
|
selectedFilterKey === 'all'
|
||||||
|
? translate('SearchAll')
|
||||||
|
: translate('SearchFiltered');
|
||||||
|
|
||||||
|
const searchSelectLabel =
|
||||||
|
selectedMovieIds.length > 0
|
||||||
|
? translate('SearchSelected')
|
||||||
|
: translate('SearchAll');
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: MOVIE_SEARCH,
|
||||||
|
movieIds: moviesToSearch,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, moviesToSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageToolbarOverflowMenuItem
|
||||||
|
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexSearchMenuItem;
|
||||||
@@ -80,14 +80,26 @@ $hoverScale: 1.05;
|
|||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overviewContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 1 1000px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
composes: link;
|
composes: link;
|
||||||
|
|
||||||
flex: 0 1 1000px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.overview {
|
.overview {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ interface CssExports {
|
|||||||
'info': string;
|
'info': string;
|
||||||
'link': string;
|
'link': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
|
'overviewContainer': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
'posterContainer': string;
|
'posterContainer': string;
|
||||||
'queue': string;
|
'queue': string;
|
||||||
|
'tags': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
'titleRow': string;
|
'titleRow': string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Icon from 'Components/Icon';
|
|||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
@@ -69,6 +70,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
|||||||
overview,
|
overview,
|
||||||
statistics = {} as Statistics,
|
statistics = {} as Statistics,
|
||||||
images,
|
images,
|
||||||
|
tags,
|
||||||
hasFile,
|
hasFile,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
tmdbId,
|
tmdbId,
|
||||||
@@ -212,15 +214,22 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<Link className={styles.overview} to={link}>
|
<div className={styles.overviewContainer}>
|
||||||
<TextTruncate
|
<Link className={styles.overview} to={link}>
|
||||||
line={Math.floor(
|
<TextTruncate
|
||||||
overviewHeight / (defaultFontSize * lineHeight)
|
line={Math.floor(
|
||||||
)}
|
overviewHeight / (defaultFontSize * lineHeight)
|
||||||
text={overview}
|
)}
|
||||||
/>
|
text={overview}
|
||||||
</Link>
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{overviewOptions.showTags ? (
|
||||||
|
<div className={styles.tags}>
|
||||||
|
<TagListConnector tags={tags} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<MovieIndexOverviewInfo
|
<MovieIndexOverviewInfo
|
||||||
height={overviewHeight}
|
height={overviewHeight}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function MovieIndexOverviewOptionsModalContent(
|
|||||||
showAdded,
|
showAdded,
|
||||||
showPath,
|
showPath,
|
||||||
showSizeOnDisk,
|
showSizeOnDisk,
|
||||||
|
showTags,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = useSelector(selectOverviewOptions);
|
} = useSelector(selectOverviewOptions);
|
||||||
|
|
||||||
@@ -161,6 +162,17 @@ function MovieIndexOverviewOptionsModalContent(
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowTags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTags"
|
||||||
|
value={showTags}
|
||||||
|
onChange={onOverviewOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,20 @@ $hoverScale: 1.05;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 0 3px;
|
||||||
|
height: 21px;
|
||||||
|
background-color: var(--movieBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagsList {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.ended {
|
.ended {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface CssExports {
|
|||||||
'overlayTitle': string;
|
'overlayTitle': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
'posterContainer': string;
|
'posterContainer': string;
|
||||||
|
'tags': string;
|
||||||
|
'tagsList': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -51,6 +52,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTags,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = useSelector(selectPosterOptions);
|
} = useSelector(selectPosterOptions);
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
certification,
|
certification,
|
||||||
originalTitle,
|
originalTitle,
|
||||||
originalLanguage,
|
originalLanguage,
|
||||||
|
tags = [],
|
||||||
} = movie;
|
} = movie;
|
||||||
|
|
||||||
const { sizeOnDisk = 0 } = statistics;
|
const { sizeOnDisk = 0 } = statistics;
|
||||||
@@ -284,6 +287,14 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showTags && tags.length ? (
|
||||||
|
<div className={styles.tags}>
|
||||||
|
<div className={styles.tagsList}>
|
||||||
|
<TagListConnector tags={tags} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<MovieIndexPosterInfo
|
<MovieIndexPosterInfo
|
||||||
studio={studio}
|
studio={studio}
|
||||||
qualityProfile={qualityProfile}
|
qualityProfile={qualityProfile}
|
||||||
@@ -306,9 +317,11 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
certification={certification}
|
certification={certification}
|
||||||
originalTitle={originalTitle}
|
originalTitle={originalTitle}
|
||||||
originalLanguage={originalLanguage}
|
originalLanguage={originalLanguage}
|
||||||
|
tags={tags}
|
||||||
showTmdbRating={showTmdbRating}
|
showTmdbRating={showTmdbRating}
|
||||||
showImdbRating={showImdbRating}
|
showImdbRating={showImdbRating}
|
||||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
|
showTags={showTags}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditMovieModalConnector
|
<EditMovieModalConnector
|
||||||
|
|||||||
@@ -9,3 +9,11 @@
|
|||||||
|
|
||||||
composes: info;
|
composes: info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
composes: tags from '~./MovieIndexPoster.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagsList {
|
||||||
|
composes: tagsList from '~./MovieIndexPoster.css';
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'info': string;
|
'info': string;
|
||||||
|
'tags': string;
|
||||||
|
'tagsList': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
@@ -28,6 +29,7 @@ interface MovieIndexPosterInfoProps {
|
|||||||
originalTitle: string;
|
originalTitle: string;
|
||||||
originalLanguage: Language;
|
originalLanguage: Language;
|
||||||
sizeOnDisk?: number;
|
sizeOnDisk?: number;
|
||||||
|
tags: number[];
|
||||||
sortKey: string;
|
sortKey: string;
|
||||||
showRelativeDates: boolean;
|
showRelativeDates: boolean;
|
||||||
showCinemaRelease: boolean;
|
showCinemaRelease: boolean;
|
||||||
@@ -38,6 +40,7 @@ interface MovieIndexPosterInfoProps {
|
|||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTags: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||||
@@ -56,6 +59,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
originalTitle,
|
originalTitle,
|
||||||
originalLanguage,
|
originalLanguage,
|
||||||
sizeOnDisk,
|
sizeOnDisk,
|
||||||
|
tags = [],
|
||||||
sortKey,
|
sortKey,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
showCinemaRelease,
|
showCinemaRelease,
|
||||||
@@ -66,6 +70,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTags,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (sortKey === 'studio' && studio) {
|
if (sortKey === 'studio' && studio) {
|
||||||
@@ -199,6 +204,16 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!showTags && sortKey === 'tags' && tags.length) {
|
||||||
|
return (
|
||||||
|
<div className={styles.tags}>
|
||||||
|
<div className={styles.tagsList}>
|
||||||
|
<TagListConnector tags={tags} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (sortKey === 'path') {
|
if (sortKey === 'path') {
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('Path')}>
|
<div className={styles.info} title={translate('Path')}>
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTags,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
const nextAiringHeight = 19;
|
const nextAiringHeight = 19;
|
||||||
@@ -191,6 +192,10 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTags) {
|
||||||
|
heights.push(21);
|
||||||
|
}
|
||||||
|
|
||||||
switch (sortKey) {
|
switch (sortKey) {
|
||||||
case 'studio':
|
case 'studio':
|
||||||
case 'added':
|
case 'added':
|
||||||
@@ -232,6 +237,11 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'tags':
|
||||||
|
if (!showTags) {
|
||||||
|
heights.push(21);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// No need to add a height of 0
|
// No need to add a height of 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTags,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
@@ -195,6 +196,18 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowTags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTags"
|
||||||
|
value={showTags}
|
||||||
|
helpText={translate('ShowTagsHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,15 @@
|
|||||||
.path {
|
.path {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
color: var(--dangerColor);
|
color: var(--dangerColor);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var(--warningColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteFilesMessage {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: var(--warningColor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'deleteFilesMessage': string;
|
||||||
'message': string;
|
'message': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
'pathContainer': string;
|
'pathContainer': string;
|
||||||
|
'statistics': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Movie from 'Movie/Movie';
|
|||||||
import { bulkDeleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
|
import { bulkDeleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
|
||||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './DeleteMovieModalContent.css';
|
import styles from './DeleteMovieModalContent.css';
|
||||||
|
|
||||||
@@ -85,9 +86,30 @@ function DeleteMovieModalContent(props: DeleteMovieModalContentProps) {
|
|||||||
onModalClose,
|
onModalClose,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { totalMovieFileCount, totalSizeOnDisk } = useMemo(() => {
|
||||||
|
return movies.reduce(
|
||||||
|
(acc, { statistics = {} }) => {
|
||||||
|
const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
|
||||||
|
|
||||||
|
acc.totalMovieFileCount += movieFileCount;
|
||||||
|
acc.totalSizeOnDisk += sizeOnDisk;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
totalMovieFileCount: 0,
|
||||||
|
totalSizeOnDisk: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [movies]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>{translate('DeleteSelectedMovie')}</ModalHeader>
|
<ModalHeader>
|
||||||
|
{movies.length > 1
|
||||||
|
? translate('DeleteSelectedMovies')
|
||||||
|
: translate('DeleteSelectedMovie')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
@@ -104,17 +126,21 @@ function DeleteMovieModalContent(props: DeleteMovieModalContentProps) {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{`Delete Movie Folder${
|
<FormLabel>
|
||||||
movies.length > 1 ? 's' : ''
|
{movies.length > 1
|
||||||
}`}</FormLabel>
|
? translate('DeleteMovieFolders')
|
||||||
|
: translate('DeleteMovieFolder')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="deleteFiles"
|
name="deleteFiles"
|
||||||
value={deleteFiles}
|
value={deleteFiles}
|
||||||
helpText={`Delete Movie Folder${
|
helpText={
|
||||||
movies.length > 1 ? 's' : ''
|
movies.length > 1
|
||||||
} and all contents`}
|
? translate('DeleteMovieFoldersHelpText')
|
||||||
|
: translate('DeleteMovieFolderHelpText')
|
||||||
|
}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onChange={onDeleteFilesChange}
|
onChange={onDeleteFilesChange}
|
||||||
/>
|
/>
|
||||||
@@ -122,26 +148,54 @@ function DeleteMovieModalContent(props: DeleteMovieModalContentProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
{`Are you sure you want to delete ${movies.length} selected movie(s)${
|
{deleteFiles
|
||||||
deleteFiles ? ' and all contents' : ''
|
? translate('DeleteMovieFolderCountWithFilesConfirmation', {
|
||||||
}?`}
|
count: movies.length,
|
||||||
|
})
|
||||||
|
: translate('DeleteMovieFolderCountConfirmation', {
|
||||||
|
count: movies.length,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{movies.map((s) => {
|
{movies.map(({ title, path, statistics = {} }) => {
|
||||||
|
const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={s.title}>
|
<li key={title}>
|
||||||
<span>{s.title}</span>
|
<span>{title}</span>
|
||||||
|
|
||||||
{deleteFiles && (
|
{deleteFiles && (
|
||||||
<span className={styles.pathContainer}>
|
<span>
|
||||||
-<span className={styles.path}>{s.path}</span>
|
<span className={styles.pathContainer}>
|
||||||
|
-<span className={styles.path}>{path}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{!!movieFileCount && (
|
||||||
|
<span className={styles.statistics}>
|
||||||
|
(
|
||||||
|
{translate('DeleteMovieFolderMovieCount', {
|
||||||
|
movieFileCount,
|
||||||
|
size: formatBytes(sizeOnDisk),
|
||||||
|
})}
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{deleteFiles && !!totalMovieFileCount ? (
|
||||||
|
<div className={styles.deleteFilesMessage}>
|
||||||
|
{translate('DeleteMovieFolderMovieCount', {
|
||||||
|
movieFileCount: totalMovieFileCount,
|
||||||
|
size: formatBytes(totalSizeOnDisk),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Icon from 'Components/Icon';
|
|||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieStatusCell.css';
|
import styles from './MovieStatusCell.css';
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.movieSearchCell {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 70px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'movieSearchCell': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieInteractiveSearchModalConnector from './Search/MovieInteractiveSearchModalConnector';
|
||||||
|
import styles from './MovieSearchCell.css';
|
||||||
|
|
||||||
|
class MovieSearchCell extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isInteractiveSearchModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onManualSearchPress = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveSearchModalClose = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
movieId,
|
||||||
|
movieTitle,
|
||||||
|
isSearching,
|
||||||
|
onSearchPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.movieSearchCell}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.SEARCH}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
onPress={onSearchPress}
|
||||||
|
title={translate('AutomaticSearch')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={this.onManualSearchPress}
|
||||||
|
title={translate('InteractiveSearch')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MovieInteractiveSearchModalConnector
|
||||||
|
isOpen={this.state.isInteractiveSearchModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
movieTitle={movieTitle}
|
||||||
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieSearchCell.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string.isRequired,
|
||||||
|
isSearching: PropTypes.bool.isRequired,
|
||||||
|
onSearchPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieSearchCell;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
|
import { isCommandExecuting } from 'Utilities/Command';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { movieId }) => movieId,
|
||||||
|
createMovieSelector(),
|
||||||
|
createCommandsSelector(),
|
||||||
|
(movieId, movie, commands) => {
|
||||||
|
const isSearching = commands.some((command) => {
|
||||||
|
const movieSearch = command.name === commandNames.MOVIE_SEARCH;
|
||||||
|
|
||||||
|
if (!movieSearch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isCommandExecuting(command) &&
|
||||||
|
command.body.movieIds.indexOf(movieId) > -1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
movieMonitored: movie.monitored,
|
||||||
|
isSearching
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onSearchPress(name, path) {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.MOVIE_SEARCH,
|
||||||
|
movieIds: [props.movieId]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieSearchCell);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'input': string;
|
'center': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
@@ -1,33 +1,115 @@
|
|||||||
import { icons } from 'Helpers/Props';
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieQuality from './MovieQuality';
|
||||||
|
import styles from './MovieStatus.css';
|
||||||
|
|
||||||
export function getMovieStatusDetails(status) {
|
function MovieStatus(props) {
|
||||||
|
const {
|
||||||
|
isAvailable,
|
||||||
|
monitored,
|
||||||
|
grabbed,
|
||||||
|
queueItem,
|
||||||
|
movieFile
|
||||||
|
} = props;
|
||||||
|
|
||||||
let statusDetails = {
|
const hasMovieFile = !!movieFile;
|
||||||
icon: icons.ANNOUNCED,
|
const isQueued = !!queueItem;
|
||||||
title: translate('Announced'),
|
|
||||||
message: translate('AnnouncedMsg')
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status === 'deleted') {
|
if (isQueued) {
|
||||||
statusDetails = {
|
const {
|
||||||
icon: icons.MOVIE_DELETED,
|
sizeleft,
|
||||||
title: translate('Deleted'),
|
size
|
||||||
message: translate('DeletedMsg')
|
} = queueItem;
|
||||||
};
|
|
||||||
} else if (status === 'inCinemas') {
|
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||||
statusDetails = {
|
|
||||||
icon: icons.IN_CINEMAS,
|
return (
|
||||||
title: translate('InCinemas'),
|
<div className={styles.center}>
|
||||||
message: translate('InCinemasMsg')
|
<QueueDetails
|
||||||
};
|
{...queueItem}
|
||||||
} else if (status === 'released') {
|
progressBar={
|
||||||
statusDetails = {
|
<ProgressBar
|
||||||
icon: icons.MOVIE_FILE,
|
progress={progress}
|
||||||
title: translate('Released'),
|
kind={kinds.PURPLE}
|
||||||
message: translate('ReleasedMsg')
|
size={sizes.MEDIUM}
|
||||||
};
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusDetails;
|
if (grabbed) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('MovieIsDownloading')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMovieFile) {
|
||||||
|
const quality = movieFile.quality;
|
||||||
|
const isCutoffNotMet = movieFile.qualityCutoffNotMet;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<MovieQuality
|
||||||
|
quality={quality}
|
||||||
|
size={movieFile.size}
|
||||||
|
isCutoffNotMet={isCutoffNotMet}
|
||||||
|
title={translate('MovieDownloaded')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monitored) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.UNMONITORED}
|
||||||
|
kind={kinds.DISABLED}
|
||||||
|
title={translate('MovieIsNotMonitored')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.MISSING}
|
||||||
|
title={translate('MovieMissingFromDisk')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.NOT_AIRED}
|
||||||
|
title={translate('MovieIsNotAvailable')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MovieStatus.propTypes = {
|
||||||
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
grabbed: PropTypes.bool,
|
||||||
|
queueItem: PropTypes.object,
|
||||||
|
movieFile: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieStatus;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import MovieStatus from 'Movie/MovieStatus';
|
||||||
|
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
||||||
|
import { createMovieByEntitySelector } from 'Store/Selectors/createMovieSelector';
|
||||||
|
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createMovieByEntitySelector(),
|
||||||
|
createQueueItemSelector(),
|
||||||
|
createMovieFileSelector(),
|
||||||
|
(movie, queueItem, movieFile) => {
|
||||||
|
const result = _.pick(movie, [
|
||||||
|
'isAvailable',
|
||||||
|
'monitored',
|
||||||
|
'grabbed'
|
||||||
|
]);
|
||||||
|
|
||||||
|
result.queueItem = queueItem;
|
||||||
|
result.movieFile = movieFile;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MovieStatusConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MovieStatus
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieStatusConnector.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieFileId: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, null)(MovieStatusConnector);
|
||||||
@@ -8,6 +8,7 @@ function MovieInteractiveSearchModal(props) {
|
|||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
movieId,
|
movieId,
|
||||||
|
movieTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ function MovieInteractiveSearchModal(props) {
|
|||||||
>
|
>
|
||||||
<MovieInteractiveSearchModalContent
|
<MovieInteractiveSearchModalContent
|
||||||
movieId={movieId}
|
movieId={movieId}
|
||||||
|
movieTitle={movieTitle}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -29,6 +31,7 @@ function MovieInteractiveSearchModal(props) {
|
|||||||
MovieInteractiveSearchModal.propTypes = {
|
MovieInteractiveSearchModal.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,17 @@ import translate from 'Utilities/String/translate';
|
|||||||
function MovieInteractiveSearchModalContent(props) {
|
function MovieInteractiveSearchModalContent(props) {
|
||||||
const {
|
const {
|
||||||
movieId,
|
movieId,
|
||||||
|
movieTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('InteractiveSearchModalHeader')}
|
{movieTitle === undefined ?
|
||||||
|
translate('InteractiveSearchModalHeader') :
|
||||||
|
translate('InteractiveSearchModalHeaderTitle', { title: movieTitle })
|
||||||
|
}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
@@ -38,6 +42,7 @@ function MovieInteractiveSearchModalContent(props) {
|
|||||||
|
|
||||||
MovieInteractiveSearchModalContent.propTypes = {
|
MovieInteractiveSearchModalContent.propTypes = {
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
export default function getMovieStatusDetails(status) {
|
||||||
|
let statusDetails = {
|
||||||
|
icon: icons.ANNOUNCED,
|
||||||
|
title: translate('Announced'),
|
||||||
|
message: translate('AnnouncedMsg')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === 'deleted') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.MOVIE_DELETED,
|
||||||
|
title: translate('Deleted'),
|
||||||
|
message: translate('DeletedMsg')
|
||||||
|
};
|
||||||
|
} else if (status === 'inCinemas') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.IN_CINEMAS,
|
||||||
|
title: translate('InCinemas'),
|
||||||
|
message: translate('InCinemasMsg')
|
||||||
|
};
|
||||||
|
} else if (status === 'released') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.MOVIE_FILE,
|
||||||
|
title: translate('Released'),
|
||||||
|
message: translate('ReleasedMsg')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusDetails;
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
export const CALENDAR = 'calendar';
|
export const CALENDAR = 'calendar';
|
||||||
export const MOVIES = 'movies';
|
export const MOVIES = 'movies';
|
||||||
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
||||||
|
export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet';
|
||||||
|
export const WANTED_MISSING = 'wanted.missing';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
CALENDAR,
|
CALENDAR,
|
||||||
MOVIES,
|
MOVIES,
|
||||||
INTERACTIVE_IMPORT
|
INTERACTIVE_IMPORT,
|
||||||
|
WANTED_CUTOFF_UNMET,
|
||||||
|
WANTED_MISSING
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function createMapStateToProps() {
|
|||||||
createMovieFileSelector(),
|
createMovieFileSelector(),
|
||||||
(movieFile) => {
|
(movieFile) => {
|
||||||
return {
|
return {
|
||||||
language: movieFile ? movieFile.language : undefined
|
languages: movieFile ? movieFile.languages : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions';
|
import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import CustomFormats from './CustomFormats';
|
import CustomFormats from './CustomFormats';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.customFormats', sortByName),
|
createSortedSectionSelector('settings.customFormats', sortByProp('name')),
|
||||||
(customFormats) => customFormats
|
(customFormats) => customFormats
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user