mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 21:13:28 -07:00
Compare commits
No commits in common. "develop" and "v2.1.7.4030" have entirely different histories.
develop
...
v2.1.7.403
722 changed files with 8725 additions and 21295 deletions
|
@ -1,13 +0,0 @@
|
||||||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
|
||||||
// the frontend has vscode settings that are distinct from the backend
|
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": ".."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../frontend"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
|
||||||
{
|
|
||||||
"name": "Lidarr",
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
|
||||||
"nodeGypDependencies": true,
|
|
||||||
"version": "20",
|
|
||||||
"nvmVersion": "latest"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forwardPorts": [8686],
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": ["esbenp.prettier-vscode"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -60,7 +60,6 @@ body:
|
||||||
- Master
|
- Master
|
||||||
- Develop
|
- Develop
|
||||||
- Nightly
|
- Nightly
|
||||||
- Plugins (experimental)
|
|
||||||
- Other (This issue will be closed)
|
- Other (This issue will be closed)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
|
@ -1,12 +0,0 @@
|
||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for more information:
|
|
||||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
# https://containers.dev/guide/dependabot
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "devcontainers"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
|
@ -12,6 +12,6 @@ jobs:
|
||||||
action:
|
action:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/label-actions@v4
|
- uses: dessant/label-actions@v3
|
||||||
with:
|
with:
|
||||||
process-only: 'issues'
|
process-only: 'issues'
|
||||||
|
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5
|
- uses: dessant/lock-threads@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
|
|
36
.gitignore
vendored
36
.gitignore
vendored
|
@ -121,13 +121,11 @@ _artifacts
|
||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
_temp*
|
|
||||||
*.Result.xml
|
*.Result.xml
|
||||||
coverage*.xml
|
coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
.mono
|
|
||||||
|
|
||||||
# VS outout folders
|
# VS outout folders
|
||||||
bin
|
bin
|
||||||
|
@ -140,6 +138,12 @@ project.fragment.lock.json
|
||||||
artifacts/
|
artifacts/
|
||||||
**/Properties/launchSettings.json
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
#VS outout folders
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
output/*
|
||||||
|
|
||||||
|
|
||||||
# macOS metadata files
|
# macOS metadata files
|
||||||
._*
|
._*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -158,12 +162,34 @@ Thumbs.db
|
||||||
/tools/Addins/*
|
/tools/Addins/*
|
||||||
packages.config.md5sum
|
packages.config.md5sum
|
||||||
|
|
||||||
|
|
||||||
|
# Common IntelliJ Platform excludes
|
||||||
|
|
||||||
|
# User specific
|
||||||
|
**/.idea/**/workspace.xml
|
||||||
|
**/.idea/**/tasks.xml
|
||||||
|
**/.idea/shelf/*
|
||||||
|
**/.idea/dictionaries
|
||||||
|
**/.idea/.idea.Radarr.Posix
|
||||||
|
**/.idea/.idea.Radarr.Windows
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
**/.idea/**/dataSources/
|
||||||
|
**/.idea/**/dataSources.ids
|
||||||
|
**/.idea/**/dataSources.xml
|
||||||
|
**/.idea/**/dataSources.local.xml
|
||||||
|
**/.idea/**/sqlDataSources.xml
|
||||||
|
**/.idea/**/dynamic.xml
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
# Rider auto-generates .iml files, and contentModel.xml
|
||||||
|
**/.idea/**/*.iml
|
||||||
|
**/.idea/**/contentModel.xml
|
||||||
|
**/.idea/**/modules.xml
|
||||||
|
|
||||||
# ignore node_modules symlink
|
# ignore node_modules symlink
|
||||||
node_modules
|
node_modules
|
||||||
node_modules.nosync
|
node_modules.nosync
|
||||||
|
|
||||||
# API doc generation
|
# API doc generation
|
||||||
.config/
|
.config/
|
||||||
|
|
||||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
|
||||||
.idea/
|
|
||||||
|
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"ms-dotnettools.csdevkit",
|
|
||||||
"ms-vscode-remote.remote-containers"
|
|
||||||
]
|
|
||||||
}
|
|
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
|
||||||
// Use hover for the description of the existing attributes
|
|
||||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
|
||||||
"name": "Run Lidarr",
|
|
||||||
"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/Lidarr",
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"stopAtEntry": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": ".NET Core Attach",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "attach"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
44
.vscode/tasks.json
vendored
44
.vscode/tasks.json
vendored
|
@ -1,44 +0,0 @@
|
||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "build dotnet",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"msbuild",
|
|
||||||
"-restore",
|
|
||||||
"${workspaceFolder}/src/Lidarr.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/Lidarr.sln",
|
|
||||||
"-property:GenerateFullPaths=true",
|
|
||||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "watch",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"watch",
|
|
||||||
"run",
|
|
||||||
"--project",
|
|
||||||
"${workspaceFolder}/src/Lidarr.sln"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Lidarr
|
# Lidarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
|
||||||
[](https://wiki.servarr.com/lidarr/installation#docker)
|
[](https://wiki.servarr.com/lidarr/installation#docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
|
@ -9,9 +8,6 @@
|
||||||
|
|
||||||
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> NOTICE - The Lidarr Metadata Server is currently down impacting adding artists, etc. Please follow [GHI 5498](https://github.com/Lidarr/Lidarr/issues/5498) or see Discord for detaila.
|
|
||||||
|
|
||||||
## Major Features Include:
|
## Major Features Include:
|
||||||
|
|
||||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||||
|
|
|
@ -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: '2.13.3'
|
majorVersion: '2.1.7'
|
||||||
minorVersion: $[counter('minorVersion', 1076)]
|
minorVersion: $[counter('minorVersion', 1076)]
|
||||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.427'
|
dotnetVersion: '6.0.417'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '16.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-22.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-11'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
|
@ -166,10 +166,10 @@ stages:
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: UseNode@1
|
- task: NodeTool@0
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
version: $(nodeVersion)
|
versionSpec: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
|
@ -1093,10 +1093,10 @@ stages:
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: UseNode@1
|
- task: NodeTool@0
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
version: $(nodeVersion)
|
versionSpec: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
|
@ -1120,19 +1120,19 @@ 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@3
|
- task: SonarCloudPrepare@1
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'cli'
|
scannerMode: 'CLI'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'lidarr_Lidarr.UI'
|
cliProjectKey: 'lidarr_Lidarr.UI'
|
||||||
cliProjectName: 'LidarrUI'
|
cliProjectName: 'LidarrUI'
|
||||||
cliProjectVersion: '$(lidarrVersion)'
|
cliProjectVersion: '$(lidarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@1
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
|
@ -1208,12 +1208,12 @@ 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@3
|
- task: SonarCloudPrepare@1
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'dotnet'
|
scannerMode: 'MSBuild'
|
||||||
projectKey: 'lidarr_Lidarr'
|
projectKey: 'lidarr_Lidarr'
|
||||||
projectName: 'Lidarr'
|
projectName: 'Lidarr'
|
||||||
projectVersion: '$(lidarrVersion)'
|
projectVersion: '$(lidarrVersion)'
|
||||||
|
@ -1226,16 +1226,21 @@ 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@3
|
- task: SonarCloudAnalyze@1
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5.3.11
|
- task: reportgenerator@4
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||||
publishCodeCoverageResults: true
|
- task: PublishCodeCoverageResults@1
|
||||||
|
displayName: Publish Coverage Report
|
||||||
|
inputs:
|
||||||
|
codeCoverageTool: 'cobertura'
|
||||||
|
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||||
|
reportDirectory: './CoverageResults/combined/'
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
|
|
|
@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||||
app_guid=${app_guid:-media}
|
app_guid=${app_guid:-media}
|
||||||
|
|
||||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||||
|
|
||||||
# Create User / Group as needed
|
# Create User / Group as needed
|
||||||
|
@ -114,7 +114,7 @@ case "$ARCH" in
|
||||||
esac
|
esac
|
||||||
echo ""
|
echo ""
|
||||||
echo "Removing previous tarballs"
|
echo "Removing previous tarballs"
|
||||||
# -f to Force so we fail if it doesn't exist
|
# -f to Force so we fail if it doesnt exist
|
||||||
rm -f "${app^}".*.tar.gz
|
rm -f "${app^}".*.tar.gz
|
||||||
echo ""
|
echo ""
|
||||||
echo "Downloading..."
|
echo "Downloading..."
|
||||||
|
|
23
docs.sh
23
docs.sh
|
@ -1,18 +1,13 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
FRAMEWORK="net6.0"
|
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
ARCHITECTURE="${2:-x64}"
|
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-$ARCHITECTURE"
|
RUNTIME="win-x64"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-$ARCHITECTURE"
|
RUNTIME="linux-x64"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-$ARCHITECTURE"
|
RUNTIME="osx-x64"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -26,21 +21,15 @@ slnFile=src/Lidarr.sln
|
||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
|
||||||
application=Lidarr.Console.dll
|
|
||||||
else
|
|
||||||
application=Lidarr.dll
|
|
||||||
fi
|
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/lidarr.console.dll" v1 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,7 @@ module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
expect: false,
|
expect: false,
|
||||||
chai: false,
|
chai: false,
|
||||||
sinon: false,
|
sinon: false
|
||||||
JSX: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (env) => {
|
||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
target: 'web',
|
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
|
@ -68,7 +67,7 @@ module.exports = (env) => {
|
||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
filename: '[name]-[contenthash].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -93,7 +92,7 @@ module.exports = (env) => {
|
||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
|
@ -135,12 +134,6 @@ module.exports = (env) => {
|
||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, 'Content/robots.txt')
|
destination: path.join(distFolder, 'Content/robots.txt')
|
||||||
},
|
|
||||||
|
|
||||||
// manifest.json and browserconfig.xml
|
|
||||||
{
|
|
||||||
source: 'frontend/src/Content/*.(json|xml)',
|
|
||||||
destination: path.join(distFolder, 'Content')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -188,7 +181,7 @@ module.exports = (env) => {
|
||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: '3.41'
|
corejs: 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -209,7 +202,7 @@ module.exports = (env) => {
|
||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,6 @@ const mixinsFiles = [
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
'autoprefixer',
|
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -172,8 +172,7 @@ function HistoryDetails(props) {
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message,
|
message
|
||||||
indexer
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -193,14 +192,6 @@ function HistoryDetails(props) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
indexer ? (
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
message ?
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|
|
@ -26,5 +26,4 @@
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 90px;
|
width: 90px;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,40 +57,30 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
iconName = icons.PAUSED;
|
iconName = icons.PAUSED;
|
||||||
title = translate('Paused');
|
title = 'Paused';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'queued') {
|
if (status === 'queued') {
|
||||||
iconName = icons.QUEUED;
|
iconName = icons.QUEUED;
|
||||||
title = translate('Queued');
|
title = 'Queued';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = translate('Downloaded');
|
title = 'Downloaded';
|
||||||
|
|
||||||
if (trackedDownloadState === 'importBlocked') {
|
|
||||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importFailed') {
|
|
||||||
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
if (trackedDownloadState === 'importPending') {
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
title += ' - Waiting to Import';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
if (trackedDownloadState === 'importing') {
|
||||||
title += ` - ${translate('Importing')}`;
|
title += ' - Importing';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
if (trackedDownloadState === 'failedPending') {
|
||||||
title += ` - ${translate('WaitingToProcess')}`;
|
title += ' - Waiting to Process';
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,38 +91,36 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
title = translate('Pending');
|
title = 'Pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = translate('PendingDownloadClientUnavailable');
|
title = 'Pending - Download client is unavailable';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'failed') {
|
if (status === 'failed') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage =
|
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||||
errorMessage || translate('CheckDownloadClientForDetails');
|
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOAD;
|
iconName = icons.DOWNLOAD;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('ImportFailed', { sourceTitle });
|
title = `Import failed: ${sourceTitle}`;
|
||||||
} else {
|
} else {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,6 @@ 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'),
|
||||||
|
@ -131,7 +130,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
: translate('BlocklistOnlyHint'),
|
: translate('BlocklistOnlyHint'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [isPending, multipleSelected]);
|
}, [multipleSelected]);
|
||||||
|
|
||||||
const handleRemovalMethodChange = useCallback(
|
const handleRemovalMethodChange = useCallback(
|
||||||
({ value }: { value: RemovalMethod }) => {
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
|
|
@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('AllAlbums')}
|
title={translate('AllAlbums')}
|
||||||
data={translate('MonitorAllAlbums')}
|
data="Monitor all new albums"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('NewAlbums')}
|
title={translate('NewAlbums')}
|
||||||
data={translate('MonitorNewAlbumsData')}
|
data="Monitor new albums released after the newest existing album"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('None')}
|
title={translate('None')}
|
||||||
data={translate('MonitorNoAlbumsData')}
|
data="Don't monitor any new albums"
|
||||||
/>
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,6 @@ export interface Statistics {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Album extends ModelBase {
|
interface Album extends ModelBase {
|
||||||
artistId: number;
|
|
||||||
artist: Artist;
|
artist: Artist;
|
||||||
foreignAlbumId: string;
|
foreignAlbumId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -20,7 +19,6 @@ interface Album extends ModelBase {
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
lastSearchTime?: string;
|
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,10 @@ import Link from 'Components/Link/Link';
|
||||||
|
|
||||||
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
||||||
const link = `/album/${foreignAlbumId}`;
|
const link = `/album/${foreignAlbumId}`;
|
||||||
const albumTitle = `${title}${disambiguation ? ` (${disambiguation})` : ''}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={link} title={albumTitle}>
|
<Link to={link}>
|
||||||
{albumTitle}
|
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
statistics = {},
|
statistics,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|
|
@ -121,8 +121,6 @@
|
||||||
|
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.sizeOnDisk,
|
.sizeOnDisk,
|
||||||
.albumType,
|
|
||||||
.secondaryTypes,
|
|
||||||
.qualityProfileName,
|
.qualityProfileName,
|
||||||
.links,
|
.links,
|
||||||
.tags {
|
.tags {
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'albumNavigationButton': string;
|
'albumNavigationButton': string;
|
||||||
'albumNavigationButtons': string;
|
'albumNavigationButtons': string;
|
||||||
'albumType': string;
|
|
||||||
'alternateTitlesIconContainer': string;
|
'alternateTitlesIconContainer': string;
|
||||||
'backdrop': string;
|
'backdrop': string;
|
||||||
'backdropOverlay': string;
|
'backdropOverlay': string;
|
||||||
|
@ -21,7 +20,6 @@ interface CssExports {
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'qualityProfileName': string;
|
'qualityProfileName': string;
|
||||||
'releaseDate': string;
|
'releaseDate': string;
|
||||||
'secondaryTypes': string;
|
|
||||||
'sizeOnDisk': string;
|
'sizeOnDisk': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
|
|
@ -192,7 +192,6 @@ class AlbumDetails extends Component {
|
||||||
duration,
|
duration,
|
||||||
overview,
|
overview,
|
||||||
albumType,
|
albumType,
|
||||||
secondaryTypes,
|
|
||||||
statistics = {},
|
statistics = {},
|
||||||
monitored,
|
monitored,
|
||||||
releaseDate,
|
releaseDate,
|
||||||
|
@ -205,7 +204,6 @@ class AlbumDetails extends Component {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
tracksError,
|
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
hasTrackFiles,
|
hasTrackFiles,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
|
@ -398,11 +396,10 @@ class AlbumDetails extends Component {
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
duration ?
|
!!duration &&
|
||||||
<span className={styles.duration}>
|
<span className={styles.duration}>
|
||||||
{formatDuration(duration)}
|
{formatDuration(duration)}
|
||||||
</span> :
|
</span>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<HeartRating
|
<HeartRating
|
||||||
|
@ -421,15 +418,14 @@ class AlbumDetails extends Component {
|
||||||
title={translate('ReleaseDate')}
|
title={translate('ReleaseDate')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.CALENDAR}
|
name={icons.CALENDAR}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.releaseDate}>
|
<span className={styles.releaseDate}>
|
||||||
{moment(releaseDate).format(shortDateFormat)}
|
{moment(releaseDate).format(shortDateFormat)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -438,15 +434,16 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.DRIVE}
|
name={icons.DRIVE}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.sizeOnDisk}>
|
<span className={styles.sizeOnDisk}>
|
||||||
{formatBytes(sizeOnDisk)}
|
{
|
||||||
|
formatBytes(sizeOnDisk || 0)
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
|
@ -462,55 +459,32 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{
|
{
|
||||||
albumType ?
|
!!albumType &&
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
title={translate('Type')}
|
title={translate('Type')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.INFO}
|
name={icons.INFO}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
<span className={styles.albumType}>
|
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
{albumType}
|
{albumType}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Label>
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
secondaryTypes.length ?
|
|
||||||
<Label
|
|
||||||
className={styles.detailsLabel}
|
|
||||||
title={translate('SecondaryTypes')}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
name={icons.INFO}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
<span className={styles.secondaryTypes}>
|
|
||||||
{secondaryTypes.join(', ')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -519,15 +493,14 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.EXTERNAL_LINK}
|
name={icons.EXTERNAL_LINK}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
{translate('Links')}
|
{translate('Links')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
|
@ -553,9 +526,8 @@ class AlbumDetails extends Component {
|
||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
!isPopulated && !albumsError && !trackFilesError &&
|
||||||
<LoadingIndicator /> :
|
<LoadingIndicator />
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -566,14 +538,6 @@ class AlbumDetails extends Component {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && tracksError ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('TracksLoadError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && trackFilesError ?
|
!isFetching && trackFilesError ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
|
@ -602,14 +566,6 @@ class AlbumDetails extends Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !media.length ?
|
|
||||||
<Alert kind={kinds.WARNING}>
|
|
||||||
{translate('NoMediumInformation')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrganizePreviewModalConnector
|
<OrganizePreviewModalConnector
|
||||||
|
@ -676,7 +632,6 @@ AlbumDetails.propTypes = {
|
||||||
duration: PropTypes.number,
|
duration: PropTypes.number,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
albumType: PropTypes.string.isRequired,
|
albumType: PropTypes.string.isRequired,
|
||||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
|
@ -703,8 +658,6 @@ AlbumDetails.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
AlbumDetails.defaultProps = {
|
AlbumDetails.defaultProps = {
|
||||||
secondaryTypes: [],
|
|
||||||
statistics: {},
|
|
||||||
isSaving: false
|
isSaving: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,16 @@ import TrackRowConnector from './TrackRowConnector';
|
||||||
import styles from './AlbumDetailsMedium.css';
|
import styles from './AlbumDetailsMedium.css';
|
||||||
|
|
||||||
function getMediumStatistics(tracks) {
|
function getMediumStatistics(tracks) {
|
||||||
const trackCount = tracks.length;
|
let trackCount = 0;
|
||||||
let trackFileCount = 0;
|
let trackFileCount = 0;
|
||||||
let totalTrackCount = 0;
|
let totalTrackCount = 0;
|
||||||
|
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
if (track.trackFileId) {
|
if (track.trackFileId) {
|
||||||
|
trackCount++;
|
||||||
trackFileCount++;
|
trackFileCount++;
|
||||||
|
} else {
|
||||||
|
trackCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTrackCount++;
|
totalTrackCount++;
|
||||||
|
|
|
@ -35,9 +35,3 @@
|
||||||
|
|
||||||
width: 55px;
|
width: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indexerFlags {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
|
|
1
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
1
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
|
@ -4,7 +4,6 @@ interface CssExports {
|
||||||
'audio': string;
|
'audio': string;
|
||||||
'customFormatScore': string;
|
'customFormatScore': string;
|
||||||
'duration': string;
|
'duration': string;
|
||||||
'indexerFlags': string;
|
|
||||||
'monitored': string;
|
'monitored': string;
|
||||||
'size': string;
|
'size': string;
|
||||||
'status': string;
|
'status': string;
|
||||||
|
|
|
@ -2,19 +2,15 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AlbumFormats from 'Album/AlbumFormats';
|
import AlbumFormats from 'Album/AlbumFormats';
|
||||||
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
||||||
import IndexerFlags from 'Album/IndexerFlags';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
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 Popover from 'Components/Tooltip/Popover';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
||||||
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import TrackActionsCell from './TrackActionsCell';
|
import TrackActionsCell from './TrackActionsCell';
|
||||||
import styles from './TrackRow.css';
|
import styles from './TrackRow.css';
|
||||||
|
|
||||||
|
@ -36,7 +32,6 @@ class TrackRow extends Component {
|
||||||
trackFileSize,
|
trackFileSize,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
indexerFlags,
|
|
||||||
columns,
|
columns,
|
||||||
deleteTrackFile
|
deleteTrackFile
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -146,30 +141,12 @@ class TrackRow extends Component {
|
||||||
customFormats.length
|
customFormats.length
|
||||||
)}
|
)}
|
||||||
tooltip={<AlbumFormats formats={customFormats} />}
|
tooltip={<AlbumFormats formats={customFormats} />}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.BOTTOM}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'indexerFlags') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexerFlags}
|
|
||||||
>
|
|
||||||
{indexerFlags ? (
|
|
||||||
<Popover
|
|
||||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
|
||||||
title={translate('IndexerFlags')}
|
|
||||||
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
|
||||||
position={tooltipPositions.LEFT}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'size') {
|
if (name === 'size') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
@ -231,14 +208,12 @@ TrackRow.propTypes = {
|
||||||
trackFileSize: PropTypes.number,
|
trackFileSize: PropTypes.number,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
indexerFlags: PropTypes.number.isRequired,
|
|
||||||
mediaInfo: PropTypes.object,
|
mediaInfo: PropTypes.object,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
TrackRow.defaultProps = {
|
TrackRow.defaultProps = {
|
||||||
customFormats: [],
|
customFormats: []
|
||||||
indexerFlags: 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TrackRow;
|
export default TrackRow;
|
||||||
|
|
|
@ -13,8 +13,7 @@ function createMapStateToProps() {
|
||||||
trackFilePath: trackFile ? trackFile.path : null,
|
trackFilePath: trackFile ? trackFile.path : null,
|
||||||
trackFileSize: trackFile ? trackFile.size : null,
|
trackFileSize: trackFile ? trackFile.size : null,
|
||||||
customFormats: trackFile ? trackFile.customFormats : [],
|
customFormats: trackFile ? trackFile.customFormats : [],
|
||||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0,
|
customFormatScore: trackFile ? trackFile.customFormatScore : 0
|
||||||
indexerFlags: trackFile ? trackFile.indexerFlags : 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
|
||||||
title,
|
title,
|
||||||
artistName,
|
artistName,
|
||||||
albumType,
|
albumType,
|
||||||
statistics = {},
|
statistics,
|
||||||
item,
|
item,
|
||||||
isSaving,
|
isSaving,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
|
||||||
|
|
||||||
interface IndexerFlagsProps {
|
|
||||||
indexerFlags: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
|
||||||
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
|
||||||
|
|
||||||
const flags = allIndexerFlags.items.filter(
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
(item) => (indexerFlags & item.id) === item.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return flags.length ? (
|
|
||||||
<ul>
|
|
||||||
{flags.map((flag, index) => {
|
|
||||||
return <li key={index}>{flag.name}</li>;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IndexerFlags;
|
|
|
@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_EXTRA_LARGE}
|
size={sizes.EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,7 +7,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function AlbumInteractiveSearchModalContent(props) {
|
function AlbumInteractiveSearchModalContent(props) {
|
||||||
const {
|
const {
|
||||||
|
@ -19,10 +18,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{albumTitle === undefined ?
|
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||||
translate('InteractiveSearchModalHeader') :
|
|
||||||
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
|
|
||||||
}
|
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
|
@ -36,7 +32,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
{translate('Close')}
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -12,10 +12,11 @@ function App({ store, history }) {
|
||||||
<DocumentTitle title={window.Lidarr.instanceName}>
|
<DocumentTitle title={window.Lidarr.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>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
import NotFound from 'Components/NotFound';
|
import NotFound from 'Components/NotFound';
|
||||||
import Switch from 'Components/Router/Switch';
|
import Switch from 'Components/Router/Switch';
|
||||||
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
||||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import Updates from 'System/Updates/Updates';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
@ -184,7 +184,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/customformats"
|
path="/settings/customformats"
|
||||||
component={CustomFormatSettingsPage}
|
component={CustomFormatSettingsConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={Updates}
|
component={UpdatesConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
50
frontend/src/App/ApplyTheme.js
Normal file
50
frontend/src/App/ApplyTheme.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
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.Lidarr.theme,
|
||||||
|
(
|
||||||
|
theme
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
theme
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme({ theme, children }) {
|
||||||
|
// Update the CSS Variables
|
||||||
|
|
||||||
|
const updateCSSVariables = useCallback(() => {
|
||||||
|
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||||
|
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||||
|
|
||||||
|
// Loop through each array key and set the CSS Variables
|
||||||
|
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||||
|
// Based on our snippet from MDN
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
`--${cssVariableKey}`,
|
||||||
|
arrayOfVariableValues[index]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
// On Component Mount and Component Update
|
||||||
|
useEffect(() => {
|
||||||
|
updateCSSVariables(theme);
|
||||||
|
}, [updateCSSVariables, theme]);
|
||||||
|
|
||||||
|
return <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTheme.propTypes = {
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(ApplyTheme);
|
|
@ -1,37 +0,0 @@
|
||||||
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.Lidarr.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,12 +1,8 @@
|
||||||
import ParseAppState from 'App/State/ParseAppState';
|
|
||||||
import AlbumAppState from './AlbumAppState';
|
import AlbumAppState from './AlbumAppState';
|
||||||
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
|
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
|
||||||
import CommandAppState from './CommandAppState';
|
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
import SystemAppState from './SystemAppState';
|
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
import TrackFilesAppState from './TrackFilesAppState';
|
import TrackFilesAppState from './TrackFilesAppState';
|
||||||
import TracksAppState from './TracksAppState';
|
import TracksAppState from './TracksAppState';
|
||||||
|
@ -44,7 +40,6 @@ export interface CustomFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
version: string;
|
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -57,16 +52,12 @@ interface AppState {
|
||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
artist: ArtistAppState;
|
artist: ArtistAppState;
|
||||||
artistIndex: ArtistIndexAppState;
|
artistIndex: ArtistIndexAppState;
|
||||||
calendar: CalendarAppState;
|
|
||||||
commands: CommandAppState;
|
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
parse: ParseAppState;
|
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
trackFiles: TrackFilesAppState;
|
trackFiles: TrackFilesAppState;
|
||||||
tracksSelection: TracksAppState;
|
tracksSelection: TracksAppState;
|
||||||
system: SystemAppState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppState;
|
export default AppState;
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import Album from 'Album/Album';
|
|
||||||
import AppSectionState, {
|
|
||||||
AppSectionFilterState,
|
|
||||||
} from 'App/State/AppSectionState';
|
|
||||||
|
|
||||||
interface CalendarAppState
|
|
||||||
extends AppSectionState<Album>,
|
|
||||||
AppSectionFilterState<Album> {}
|
|
||||||
|
|
||||||
export default CalendarAppState;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import AppSectionState from 'App/State/AppSectionState';
|
|
||||||
import Command from 'Commands/Command';
|
|
||||||
|
|
||||||
export type CommandAppState = AppSectionState<Command>;
|
|
||||||
|
|
||||||
export default CommandAppState;
|
|
|
@ -1,35 +0,0 @@
|
||||||
import Album from 'Album/Album';
|
|
||||||
import ModelBase from 'App/ModelBase';
|
|
||||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
|
||||||
import Artist from 'Artist/Artist';
|
|
||||||
import { QualityModel } from 'Quality/Quality';
|
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
|
|
||||||
export interface ArtistTitleInfo {
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParsedAlbumInfo {
|
|
||||||
albumTitle: string;
|
|
||||||
artistName: string;
|
|
||||||
artistTitleInfo: ArtistTitleInfo;
|
|
||||||
discography: boolean;
|
|
||||||
quality: QualityModel;
|
|
||||||
releaseGroup?: string;
|
|
||||||
releaseHash: string;
|
|
||||||
releaseTitle: string;
|
|
||||||
releaseTokens: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParseModel extends ModelBase {
|
|
||||||
title: string;
|
|
||||||
parsedAlbumInfo: ParsedAlbumInfo;
|
|
||||||
artist?: Artist;
|
|
||||||
albums: Album[];
|
|
||||||
customFormats?: CustomFormat[];
|
|
||||||
customFormatScore?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParseAppState = AppSectionItemState<ParseModel>;
|
|
||||||
|
|
||||||
export default ParseAppState;
|
|
|
@ -1,28 +1,22 @@
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionItemState,
|
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
import IndexerFlag from 'typings/IndexerFlag';
|
|
||||||
import MetadataProfile from 'typings/MetadataProfile';
|
import MetadataProfile from 'typings/MetadataProfile';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import RootFolder from 'typings/RootFolder';
|
import RootFolder from 'typings/RootFolder';
|
||||||
import General from 'typings/Settings/General';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export type GeneralAppState = AppSectionItemState<General>;
|
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
@ -45,32 +39,22 @@ export interface MetadataProfilesAppState
|
||||||
extends AppSectionState<MetadataProfile>,
|
extends AppSectionState<MetadataProfile>,
|
||||||
AppSectionSchemaState<MetadataProfile> {}
|
AppSectionSchemaState<MetadataProfile> {}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
|
||||||
extends AppSectionState<CustomFormat>,
|
|
||||||
AppSectionDeleteState,
|
|
||||||
AppSectionSaveState {}
|
|
||||||
|
|
||||||
export interface RootFolderAppState
|
export interface RootFolderAppState
|
||||||
extends AppSectionState<RootFolder>,
|
extends AppSectionState<RootFolder>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
|
||||||
customFormats: CustomFormatAppState;
|
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
general: GeneralAppState;
|
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
metadataProfiles: MetadataProfilesAppState;
|
metadataProfiles: MetadataProfilesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
ui: UiSettingsAppState;
|
uiSettings: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsAppState;
|
export default SettingsAppState;
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
|
||||||
import Update from 'typings/Update';
|
|
||||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
|
||||||
|
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
|
||||||
export type UpdateAppState = AppSectionState<Update>;
|
|
||||||
|
|
||||||
interface SystemAppState {
|
|
||||||
updates: UpdateAppState;
|
|
||||||
status: SystemStatusAppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SystemAppState;
|
|
|
@ -1,32 +1,12 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState,
|
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
|
|
||||||
export interface Tag extends ModelBase {
|
export interface Tag extends ModelBase {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagDetail extends ModelBase {
|
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||||
label: string;
|
|
||||||
autoTagIds: number[];
|
|
||||||
delayProfileIds: number[];
|
|
||||||
downloadClientIds: [];
|
|
||||||
importListIds: number[];
|
|
||||||
indexerIds: number[];
|
|
||||||
notificationIds: number[];
|
|
||||||
restrictionIds: number[];
|
|
||||||
artistIds: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TagDetailAppState
|
|
||||||
extends AppSectionState<TagDetail>,
|
|
||||||
AppSectionDeleteState,
|
|
||||||
AppSectionSaveState {}
|
|
||||||
|
|
||||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
|
|
||||||
details: TagDetailAppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TagsAppState;
|
export default TagsAppState;
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface Ratings {
|
||||||
|
|
||||||
interface Artist extends ModelBase {
|
interface Artist extends ModelBase {
|
||||||
added: string;
|
added: string;
|
||||||
|
artistMetadataId: string;
|
||||||
foreignArtistId: string;
|
foreignArtistId: string;
|
||||||
cleanName: string;
|
cleanName: string;
|
||||||
ended: boolean;
|
ended: boolean;
|
||||||
|
|
|
@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
{translate('Close')}
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onDeleteArtistConfirmed}
|
onPress={this.onDeleteArtistConfirmed}
|
||||||
>
|
>
|
||||||
{translate('Delete')}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@ -161,7 +161,9 @@ DeleteArtistModalContent.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
DeleteArtistModalContent.defaultProps = {
|
DeleteArtistModalContent.defaultProps = {
|
||||||
statistics: {}
|
statistics: {
|
||||||
|
trackFileCount: 0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteArtistModalContent;
|
export default DeleteArtistModalContent;
|
||||||
|
|
|
@ -10,7 +10,6 @@ function AlbumGroupInfo(props) {
|
||||||
const {
|
const {
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
albumFileCount,
|
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
sizeOnDisk
|
sizeOnDisk
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -31,13 +30,6 @@ function AlbumGroupInfo(props) {
|
||||||
data={monitoredAlbumCount}
|
data={monitoredAlbumCount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
titleClassName={styles.title}
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('WithFiles')}
|
|
||||||
data={albumFileCount}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
titleClassName={styles.title}
|
titleClassName={styles.title}
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
|
@ -58,7 +50,6 @@ function AlbumGroupInfo(props) {
|
||||||
AlbumGroupInfo.propTypes = {
|
AlbumGroupInfo.propTypes = {
|
||||||
totalAlbumCount: PropTypes.number.isRequired,
|
totalAlbumCount: PropTypes.number.isRequired,
|
||||||
monitoredAlbumCount: PropTypes.number.isRequired,
|
monitoredAlbumCount: PropTypes.number.isRequired,
|
||||||
albumFileCount: PropTypes.number.isRequired,
|
|
||||||
trackFileCount: PropTypes.number.isRequired,
|
trackFileCount: PropTypes.number.isRequired,
|
||||||
sizeOnDisk: PropTypes.number.isRequired
|
sizeOnDisk: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -149,7 +149,9 @@ class AlbumRow extends Component {
|
||||||
if (name === 'secondaryTypes') {
|
if (name === 'secondaryTypes') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{secondaryTypes.join(', ')}
|
{
|
||||||
|
secondaryTypes
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -158,7 +160,7 @@ class AlbumRow extends Component {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{
|
{
|
||||||
totalTrackCount
|
statistics.totalTrackCount
|
||||||
}
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
|
|
|
@ -192,7 +192,7 @@ class ArtistDetails extends Component {
|
||||||
artistName,
|
artistName,
|
||||||
ratings,
|
ratings,
|
||||||
path,
|
path,
|
||||||
statistics = {},
|
statistics,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
monitored,
|
monitored,
|
||||||
genres,
|
genres,
|
||||||
|
|
|
@ -22,43 +22,32 @@ import styles from './ArtistDetailsSeason.css';
|
||||||
|
|
||||||
function getAlbumStatistics(albums) {
|
function getAlbumStatistics(albums) {
|
||||||
let albumCount = 0;
|
let albumCount = 0;
|
||||||
let albumFileCount = 0;
|
|
||||||
let trackFileCount = 0;
|
let trackFileCount = 0;
|
||||||
let totalAlbumCount = 0;
|
let totalAlbumCount = 0;
|
||||||
let monitoredAlbumCount = 0;
|
let monitoredAlbumCount = 0;
|
||||||
let hasMonitoredAlbums = false;
|
let hasMonitoredAlbums = false;
|
||||||
let sizeOnDisk = 0;
|
let sizeOnDisk = 0;
|
||||||
|
|
||||||
albums.forEach(({ monitored, releaseDate, statistics = {} }) => {
|
albums.forEach((album) => {
|
||||||
const {
|
if (album.statistics) {
|
||||||
trackFileCount: albumTrackFileCount = 0,
|
sizeOnDisk = sizeOnDisk + album.statistics.sizeOnDisk;
|
||||||
totalTrackCount: albumTotalTrackCount = 0,
|
trackFileCount = trackFileCount + album.statistics.trackFileCount;
|
||||||
sizeOnDisk: albumSizeOnDisk = 0
|
|
||||||
} = statistics;
|
|
||||||
|
|
||||||
const hasFiles = albumTrackFileCount > 0 && albumTrackFileCount === albumTotalTrackCount;
|
if (album.statistics.trackFileCount === album.statistics.totalTrackCount || (album.monitored && isBefore(album.airDateUtc))) {
|
||||||
|
|
||||||
if (hasFiles || (monitored && isBefore(releaseDate))) {
|
|
||||||
albumCount++;
|
albumCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFiles) {
|
|
||||||
albumFileCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monitored) {
|
if (album.monitored) {
|
||||||
monitoredAlbumCount++;
|
monitoredAlbumCount++;
|
||||||
hasMonitoredAlbums = true;
|
hasMonitoredAlbums = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalAlbumCount++;
|
totalAlbumCount++;
|
||||||
trackFileCount = trackFileCount + albumTrackFileCount;
|
|
||||||
sizeOnDisk = sizeOnDisk + albumSizeOnDisk;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount,
|
albumCount,
|
||||||
albumFileCount,
|
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
|
@ -67,8 +56,8 @@ function getAlbumStatistics(albums) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAlbumCountKind(monitored, albumCount, albumFileCount) {
|
function getAlbumCountKind(monitored, albumCount, monitoredAlbumCount) {
|
||||||
if (albumCount === albumFileCount && albumFileCount > 0) {
|
if (albumCount === monitoredAlbumCount && monitoredAlbumCount > 0) {
|
||||||
return kinds.SUCCESS;
|
return kinds.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +192,6 @@ class ArtistDetailsSeason extends Component {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
albumCount,
|
albumCount,
|
||||||
albumFileCount,
|
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
|
@ -238,9 +226,9 @@ class ArtistDetailsSeason extends Component {
|
||||||
anchor={
|
anchor={
|
||||||
<Label
|
<Label
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, albumFileCount)}
|
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, monitoredAlbumCount)}
|
||||||
>
|
>
|
||||||
<span>{albumFileCount} / {albumCount}</span>
|
<span>{albumCount} / {monitoredAlbumCount}</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
title={translate('GroupInformation')}
|
title={translate('GroupInformation')}
|
||||||
|
@ -249,7 +237,6 @@ class ArtistDetailsSeason extends Component {
|
||||||
<AlbumGroupInfo
|
<AlbumGroupInfo
|
||||||
totalAlbumCount={totalAlbumCount}
|
totalAlbumCount={totalAlbumCount}
|
||||||
monitoredAlbumCount={monitoredAlbumCount}
|
monitoredAlbumCount={monitoredAlbumCount}
|
||||||
albumFileCount={albumFileCount}
|
|
||||||
trackFileCount={trackFileCount}
|
trackFileCount={trackFileCount}
|
||||||
sizeOnDisk={sizeOnDisk}
|
sizeOnDisk={sizeOnDisk}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import ArtistTags from './ArtistTags';
|
import ArtistTags from './ArtistTags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -13,8 +12,8 @@ function createMapStateToProps() {
|
||||||
const tags = artist.tags
|
const tags = artist.tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((tag) => !!tag)
|
.filter((tag) => !!tag)
|
||||||
.sort(sortByProp('label'))
|
.map((tag) => tag.label)
|
||||||
.map((tag) => tag.label);
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags
|
tags
|
||||||
|
|
|
@ -15,7 +15,7 @@ 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 Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditArtistModalContent.css';
|
import styles from './EditArtistModalContent.css';
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form {...otherProps}>
|
<Form {...otherProps}>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Monitored')}
|
{translate('Monitored')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -107,10 +107,9 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MonitorNewItems')}
|
{translate('MonitorNewItems')}
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -133,7 +132,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('QualityProfile')}
|
{translate('QualityProfile')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -147,10 +146,10 @@ class EditArtistModalContent extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
showMetadataProfile ?
|
showMetadataProfile &&
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MetadataProfile')}
|
Metadata Profile
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
|
@ -174,11 +173,10 @@ class EditArtistModalContent extends Component {
|
||||||
{...metadataProfileId}
|
{...metadataProfileId}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup> :
|
</FormGroup>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Path')}
|
{translate('Path')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -191,7 +189,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Tags')}
|
{translate('Tags')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -211,7 +209,7 @@ class EditArtistModalContent extends Component {
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={onDeleteArtistPress}
|
onPress={onDeleteArtistPress}
|
||||||
>
|
>
|
||||||
{translate('Delete')}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Statistics } from 'Album/Album';
|
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
@ -57,7 +56,7 @@ function AlbumDetails(props: AlbumDetailsProps) {
|
||||||
disambiguation,
|
disambiguation,
|
||||||
albumType,
|
albumType,
|
||||||
monitored,
|
monitored,
|
||||||
statistics = {} as Statistics,
|
statistics,
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
} = album;
|
} = album;
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ const monitoredOptions = [
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
isDisabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'monitored',
|
key: 'monitored',
|
||||||
|
|
|
@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_EXTRA_LARGE}
|
size={sizes.EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
|
||||||
import { setCalendarFilter } from 'Store/Actions/calendarActions';
|
|
||||||
|
|
||||||
function createCalendarSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.calendar.items,
|
|
||||||
(calendar) => {
|
|
||||||
return calendar;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFilterBuilderPropsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.calendar.filterBuilderProps,
|
|
||||||
(filterBuilderProps) => {
|
|
||||||
return filterBuilderProps;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CalendarFilterModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
|
||||||
const sectionItems = useSelector(createCalendarSelector());
|
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
|
||||||
const customFilterType = 'calendar';
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const dispatchSetFilter = useCallback(
|
|
||||||
(payload: unknown) => {
|
|
||||||
dispatch(setCalendarFilter(payload));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterModal
|
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
|
||||||
sectionItems={sectionItems}
|
|
||||||
filterBuilderProps={filterBuilderProps}
|
|
||||||
customFilterType={customFilterType}
|
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -14,7 +14,6 @@ import { align, icons } from 'Helpers/Props';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CalendarConnector from './CalendarConnector';
|
import CalendarConnector from './CalendarConnector';
|
||||||
import CalendarFilterModal from './CalendarFilterModal';
|
|
||||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||||
import LegendConnector from './Legend/LegendConnector';
|
import LegendConnector from './Legend/LegendConnector';
|
||||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||||
|
@ -79,7 +78,6 @@ class CalendarPage extends Component {
|
||||||
const {
|
const {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
customFilters,
|
|
||||||
hasArtist,
|
hasArtist,
|
||||||
artistError,
|
artistError,
|
||||||
artistIsFetching,
|
artistIsFetching,
|
||||||
|
@ -139,8 +137,7 @@ class CalendarPage extends Component {
|
||||||
isDisabled={!hasArtist}
|
isDisabled={!hasArtist}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={customFilters}
|
customFilters={[]}
|
||||||
filterModalConnectorComponent={CalendarFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
@ -207,7 +204,6 @@ class CalendarPage extends Component {
|
||||||
CalendarPage.propTypes = {
|
CalendarPage.propTypes = {
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hasArtist: PropTypes.bool.isRequired,
|
hasArtist: PropTypes.bool.isRequired,
|
||||||
artistError: PropTypes.object,
|
artistError: PropTypes.object,
|
||||||
artistIsFetching: PropTypes.bool.isRequired,
|
artistIsFetching: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
@ -60,7 +59,6 @@ function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.calendar.selectedFilterKey,
|
(state) => state.calendar.selectedFilterKey,
|
||||||
(state) => state.calendar.filters,
|
(state) => state.calendar.filters,
|
||||||
createCustomFiltersSelector('calendar'),
|
|
||||||
createArtistCountSelector(),
|
createArtistCountSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
createMissingAlbumIdsSelector(),
|
createMissingAlbumIdsSelector(),
|
||||||
|
@ -69,7 +67,6 @@ function createMapStateToProps() {
|
||||||
(
|
(
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
customFilters,
|
|
||||||
artistCount,
|
artistCount,
|
||||||
uiSettings,
|
uiSettings,
|
||||||
missingAlbumIds,
|
missingAlbumIds,
|
||||||
|
@ -79,7 +76,6 @@ function createMapStateToProps() {
|
||||||
return {
|
return {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
customFilters,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
hasArtist: !!artistCount.count,
|
hasArtist: !!artistCount.count,
|
||||||
artistError: artistCount.error,
|
artistError: artistCount.error,
|
||||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
helpText={translate('ICalTagsArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -13,7 +13,6 @@ export interface CommandBody {
|
||||||
trigger: string;
|
trigger: string;
|
||||||
suppressMessages: boolean;
|
suppressMessages: boolean;
|
||||||
artistId?: number;
|
artistId?: number;
|
||||||
artistIds?: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Command extends ModelBase {
|
interface Command extends ModelBase {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import PathInput from 'Components/Form/PathInput';
|
import PathInput from 'Components/Form/PathInput';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
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';
|
||||||
|
@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
|
||||||
className={styles.mappedDrivesWarning}
|
className={styles.mappedDrivesWarning}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/lidarr/faq#why-cant-lidarr-see-my-files-on-a-remote-server' })} />
|
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/lidarr/faq">FAQ</Link> for more information.
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ function ArtistFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
|
||||||
const tagList = allArtists
|
const tagList = allArtists
|
||||||
.map((artist) => ({ id: artist.id, name: artist.artistName }))
|
.map((artist) => ({ id: artist.id, name: artist.artistName }))
|
||||||
.sort(sortByProp('name'));
|
.sort(sortByName);
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { maxBy } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
@ -51,7 +50,7 @@ class FilterBuilderModalContent extends Component {
|
||||||
if (id) {
|
if (id) {
|
||||||
dispatchSetFilter({ selectedFilterKey: id });
|
dispatchSetFilter({ selectedFilterKey: id });
|
||||||
} else {
|
} else {
|
||||||
const last = maxBy(customFilters, 'id');
|
const last = customFilters[customFilters.length -1];
|
||||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +108,7 @@ class FilterBuilderModalContent extends Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
labelErrors: [
|
labelErrors: [
|
||||||
{
|
{
|
||||||
message: translate('LabelIsRequired')
|
message: 'Label is required'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -147,7 +146,7 @@ class FilterBuilderModalContent extends Component {
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('CustomFilter')}
|
Custom Filter
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
@ -167,9 +166,7 @@ class FilterBuilderModalContent extends Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>{translate('Filters')}</div>
|
||||||
{translate('Filters')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.rows}>
|
<div className={styles.rows}>
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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 ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue';
|
import ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue';
|
||||||
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
|
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
|
||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
|
@ -11,11 +10,11 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue';
|
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
|
||||||
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||||
import styles from './FilterBuilderRow.css';
|
import styles from './FilterBuilderRow.css';
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
return IndexerFilterBuilderRowValueConnector;
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.METADATA_PROFILE:
|
case filterBuilderValueTypes.METADATA_PROFILE:
|
||||||
return MetadataProfileFilterBuilderRowValue;
|
return MetadataProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
||||||
return MonitorNewItemsFilterBuilderRowValue;
|
return MonitorNewItemsFilterBuilderRowValue;
|
||||||
|
@ -80,7 +79,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
return QualityFilterBuilderRowValueConnector;
|
return QualityFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValue;
|
return QualityProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.ARTIST:
|
case filterBuilderValueTypes.ARTIST:
|
||||||
return ArtistFilterBuilderRowValue;
|
return ArtistFilterBuilderRowValue;
|
||||||
|
@ -225,7 +224,7 @@ class FilterBuilderRow extends Component {
|
||||||
key: name,
|
key: name,
|
||||||
value: typeof label === 'function' ? label() : label
|
value: typeof label === 'function' ? label() : label
|
||||||
};
|
};
|
||||||
}).sort(sortByProp('value'));
|
}).sort((a, b) => a.value.localeCompare(b.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 sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
function createTagListSelector() {
|
function createTagListSelector() {
|
||||||
|
@ -38,7 +38,7 @@ function createTagListSelector() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []).sort(sortByProp('name'));
|
}, []).sort(sortByName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.uniqBy(items, 'id');
|
return _.uniqBy(items, 'id');
|
||||||
|
|
|
@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
get name() {
|
get name() {
|
||||||
return translate('ImportCompleteFailed');
|
return translate('ImportFailed');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
|
||||||
|
|
||||||
function createMetadataProfilesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.settings.metadataProfiles.items,
|
|
||||||
(metadataProfiles) => {
|
|
||||||
return metadataProfiles;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MetadataProfileFilterBuilderRowValue(
|
|
||||||
props: FilterBuilderRowValueProps
|
|
||||||
) {
|
|
||||||
const metadataProfiles = useSelector(createMetadataProfilesSelector());
|
|
||||||
|
|
||||||
const tagList = metadataProfiles
|
|
||||||
.map(({ id, name }) => ({ id, name }))
|
|
||||||
.sort(sortByProp('name'));
|
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MetadataProfileFilterBuilderRowValue;
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.metadataProfiles,
|
||||||
|
(metadataProfiles) => {
|
||||||
|
const tagList = metadataProfiles.items.map((metadataProfile) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
} = metadataProfile;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -1,30 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
|
||||||
|
|
||||||
function createQualityProfilesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.settings.qualityProfiles.items,
|
|
||||||
(qualityProfiles) => {
|
|
||||||
return qualityProfiles;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function QualityProfileFilterBuilderRowValue(
|
|
||||||
props: FilterBuilderRowValueProps
|
|
||||||
) {
|
|
||||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
|
||||||
|
|
||||||
const tagList = qualityProfiles
|
|
||||||
.map(({ id, name }) => ({ id, name }))
|
|
||||||
.sort(sortByProp('name'));
|
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QualityProfileFilterBuilderRowValue;
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.qualityProfiles,
|
||||||
|
(qualityProfiles) => {
|
||||||
|
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
} = qualityProfile;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
||||||
dispatchSetFilter
|
dispatchSetFilter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Assume that delete and then unmounting means the deletion was successful.
|
// Assume that delete and then unmounting means the delete was successful.
|
||||||
// Moving this check to an ancestor would be more accurate, but would have
|
// Moving this check to a ancestor would be more accurate, but would have
|
||||||
// more boilerplate.
|
// more boilerplate.
|
||||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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';
|
||||||
|
@ -32,7 +31,7 @@ function CustomFiltersModalContent(props) {
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
.map((customFilter) => {
|
.map((customFilter) => {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import TagInputConnector from './TagInputConnector';
|
|
||||||
|
|
||||||
interface ArtistTagInputProps {
|
|
||||||
name: string;
|
|
||||||
value: number | number[];
|
|
||||||
onChange: ({
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
value: number | number[];
|
|
||||||
}) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ArtistTagInput(props: ArtistTagInputProps) {
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,8 +4,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 { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -23,18 +22,17 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
||||||
|
|
||||||
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||||
return {
|
return {
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name,
|
value: downloadClient.name
|
||||||
hint: `(${downloadClient.id})`
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (includeAny) {
|
if (includeAny) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 0,
|
key: 0,
|
||||||
value: `(${translate('Any')})`
|
value: '(Any)'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
.isDisabled {
|
.isDisabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownArrowContainer {
|
.dropdownArrowContainer {
|
||||||
|
|
|
@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
}
|
}
|
||||||
|
@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
bottom
|
||||||
|
} = data.offsets.reference;
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
if ((/^botton/).test(data.placement)) {
|
||||||
|
data.styles.maxHeight = windowHeight - bottom;
|
||||||
|
} else {
|
||||||
|
data.styles.maxHeight = top;
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component {
|
||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: true,
|
|
||||||
boundariesElement: 'viewport'
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import Link from 'Components/Link/Link';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
|
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
|
||||||
import ArtistTagInput from './ArtistTagInput';
|
|
||||||
import AutoCompleteInput from './AutoCompleteInput';
|
import AutoCompleteInput from './AutoCompleteInput';
|
||||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||||
import CheckInput from './CheckInput';
|
import CheckInput from './CheckInput';
|
||||||
|
@ -13,7 +12,6 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
||||||
|
@ -49,12 +47,12 @@ function getComponent(type) {
|
||||||
case inputTypes.DEVICE:
|
case inputTypes.DEVICE:
|
||||||
return DeviceInputConnector;
|
return DeviceInputConnector;
|
||||||
|
|
||||||
case inputTypes.KEY_VALUE_LIST:
|
|
||||||
return KeyValueListInput;
|
|
||||||
|
|
||||||
case inputTypes.PLAYLIST:
|
case inputTypes.PLAYLIST:
|
||||||
return PlaylistInputConnector;
|
return PlaylistInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.KEY_VALUE_LIST:
|
||||||
|
return KeyValueListInput;
|
||||||
|
|
||||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||||
return MonitorAlbumsSelectInput;
|
return MonitorAlbumsSelectInput;
|
||||||
|
|
||||||
|
@ -85,9 +83,6 @@ function getComponent(type) {
|
||||||
case inputTypes.INDEXER_SELECT:
|
case inputTypes.INDEXER_SELECT:
|
||||||
return IndexerSelectInputConnector;
|
return IndexerSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
|
||||||
return IndexerFlagsSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||||
return DownloadClientSelectInputConnector;
|
return DownloadClientSelectInputConnector;
|
||||||
|
|
||||||
|
@ -100,9 +95,6 @@ function getComponent(type) {
|
||||||
case inputTypes.DYNAMIC_SELECT:
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
return EnhancedSelectInputConnector;
|
return EnhancedSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.ARTIST_TAG:
|
|
||||||
return ArtistTagInput;
|
|
||||||
|
|
||||||
case inputTypes.SERIES_TYPE_SELECT:
|
case inputTypes.SERIES_TYPE_SELECT:
|
||||||
return SeriesTypeSelectInput;
|
return SeriesTypeSelectInput;
|
||||||
|
|
||||||
|
@ -300,7 +292,6 @@ FormInputGroup.propTypes = {
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
includeNone: PropTypes.bool,
|
includeNone: PropTypes.bool,
|
||||||
selectedValueOptions: PropTypes.object,
|
selectedValueOptions: PropTypes.object,
|
||||||
indexerFlags: PropTypes.number,
|
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
|
||||||
createSelector(
|
|
||||||
(state: AppState) => state.settings.indexerFlags,
|
|
||||||
(indexerFlags) => {
|
|
||||||
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
if ((selectedFlags & id) === id) {
|
|
||||||
acc.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const values = indexerFlags.items.map(({ id, name }) => ({
|
|
||||||
key: id,
|
|
||||||
value: name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
interface IndexerFlagsSelectInputProps {
|
|
||||||
name: string;
|
|
||||||
indexerFlags: number;
|
|
||||||
onChange(payload: object): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
|
||||||
const { indexerFlags, onChange } = props;
|
|
||||||
|
|
||||||
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
|
||||||
|
|
||||||
const onChangeWrapper = useCallback(
|
|
||||||
({ name, value }: { name: string; value: number[] }) => {
|
|
||||||
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
|
||||||
|
|
||||||
onChange({ name, value: indexerFlags });
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...props}
|
|
||||||
value={value}
|
|
||||||
values={values}
|
|
||||||
onChange={onChangeWrapper}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IndexerFlagsSelectInput;
|
|
|
@ -4,8 +4,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 sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -20,7 +19,7 @@ function createMapStateToProps() {
|
||||||
items
|
items
|
||||||
} = indexers;
|
} = indexers;
|
||||||
|
|
||||||
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
|
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||||
return {
|
return {
|
||||||
key: indexer.id,
|
key: indexer.id,
|
||||||
value: indexer.name
|
value: indexer.name
|
||||||
|
@ -30,7 +29,7 @@ function createMapStateToProps() {
|
||||||
if (includeAny) {
|
if (includeAny) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 0,
|
key: 0,
|
||||||
value: `(${translate('Any')})`
|
value: '(Any)'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
156
frontend/src/Components/Form/KeyValueListInput.js
Normal file
156
frontend/src/Components/Form/KeyValueListInput.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import KeyValueListInputItem from './KeyValueListInputItem';
|
||||||
|
import styles from './KeyValueListInput.css';
|
||||||
|
|
||||||
|
class KeyValueListInput extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isFocused: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onItemChange = (index, itemValue) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = [...value];
|
||||||
|
|
||||||
|
if (index == null) {
|
||||||
|
newValue.push(itemValue);
|
||||||
|
} else {
|
||||||
|
newValue.splice(index, 1, itemValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveItem = (index) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
this.setState({
|
||||||
|
isFocused: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.setState({
|
||||||
|
isFocused: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = value.reduce((acc, v) => {
|
||||||
|
if (v.key || v.value) {
|
||||||
|
acc.push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (newValue.length !== value.length) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
keyPlaceholder,
|
||||||
|
valuePlaceholder,
|
||||||
|
hasError,
|
||||||
|
hasWarning
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { isFocused } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(
|
||||||
|
className,
|
||||||
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
[...value, { key: '', value: '' }].map((v, index) => {
|
||||||
|
return (
|
||||||
|
<KeyValueListInputItem
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
keyValue={v.key}
|
||||||
|
value={v.value}
|
||||||
|
keyPlaceholder={keyPlaceholder}
|
||||||
|
valuePlaceholder={valuePlaceholder}
|
||||||
|
isNew={index === value.length}
|
||||||
|
onChange={this.onItemChange}
|
||||||
|
onRemove={this.onRemoveItem}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValueListInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
keyPlaceholder: PropTypes.string,
|
||||||
|
valuePlaceholder: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyValueListInput.defaultProps = {
|
||||||
|
className: styles.inputContainer,
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyValueListInput;
|
|
@ -1,104 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { InputOnChange } from 'typings/inputs';
|
|
||||||
import KeyValueListInputItem from './KeyValueListInputItem';
|
|
||||||
import styles from './KeyValueListInput.css';
|
|
||||||
|
|
||||||
interface KeyValue {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KeyValueListInputProps {
|
|
||||||
className?: string;
|
|
||||||
name: string;
|
|
||||||
value: KeyValue[];
|
|
||||||
hasError?: boolean;
|
|
||||||
hasWarning?: boolean;
|
|
||||||
keyPlaceholder?: string;
|
|
||||||
valuePlaceholder?: string;
|
|
||||||
onChange: InputOnChange<KeyValue[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function KeyValueListInput({
|
|
||||||
className = styles.inputContainer,
|
|
||||||
name,
|
|
||||||
value = [],
|
|
||||||
hasError = false,
|
|
||||||
hasWarning = false,
|
|
||||||
keyPlaceholder,
|
|
||||||
valuePlaceholder,
|
|
||||||
onChange,
|
|
||||||
}: KeyValueListInputProps): JSX.Element {
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
|
|
||||||
const handleItemChange = useCallback(
|
|
||||||
(index: number | null, itemValue: KeyValue) => {
|
|
||||||
const newValue = [...value];
|
|
||||||
|
|
||||||
if (index === null) {
|
|
||||||
newValue.push(itemValue);
|
|
||||||
} else {
|
|
||||||
newValue.splice(index, 1, itemValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({ name, value: newValue });
|
|
||||||
},
|
|
||||||
[value, name, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemoveItem = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const newValue = [...value];
|
|
||||||
newValue.splice(index, 1);
|
|
||||||
onChange({ name, value: newValue });
|
|
||||||
},
|
|
||||||
[value, name, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFocus = useCallback(() => setIsFocused(true), []);
|
|
||||||
|
|
||||||
const onBlur = useCallback(() => {
|
|
||||||
setIsFocused(false);
|
|
||||||
|
|
||||||
const newValue = value.reduce((acc: KeyValue[], v) => {
|
|
||||||
if (v.key || v.value) {
|
|
||||||
acc.push(v);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (newValue.length !== value.length) {
|
|
||||||
onChange({ name, value: newValue });
|
|
||||||
}
|
|
||||||
}, [value, name, onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isFocused && styles.isFocused,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{[...value, { key: '', value: '' }].map((v, index) => (
|
|
||||||
<KeyValueListInputItem
|
|
||||||
key={index}
|
|
||||||
index={index}
|
|
||||||
keyValue={v.key}
|
|
||||||
value={v.value}
|
|
||||||
keyPlaceholder={keyPlaceholder}
|
|
||||||
valuePlaceholder={valuePlaceholder}
|
|
||||||
isNew={index === value.length}
|
|
||||||
onChange={handleItemChange}
|
|
||||||
onRemove={handleRemoveItem}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyValueListInput;
|
|
|
@ -5,19 +5,13 @@
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
border-bottom: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyInputWrapper {
|
.inputWrapper {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.valueInputWrapper {
|
|
||||||
flex: 1 0 0;
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWrapper {
|
.buttonWrapper {
|
||||||
flex: 0 0 22px;
|
flex: 0 0 22px;
|
||||||
}
|
}
|
||||||
|
@ -26,10 +20,6 @@
|
||||||
.valueInput {
|
.valueInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: var(--inputBackgroundColor);
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--helpTextColor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'buttonWrapper': string;
|
'buttonWrapper': string;
|
||||||
|
'inputWrapper': string;
|
||||||
'itemContainer': string;
|
'itemContainer': string;
|
||||||
'keyInput': string;
|
'keyInput': string;
|
||||||
'keyInputWrapper': string;
|
|
||||||
'valueInput': string;
|
'valueInput': string;
|
||||||
'valueInputWrapper': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
124
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
124
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './KeyValueListInputItem.css';
|
||||||
|
|
||||||
|
class KeyValueListInputItem extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onKeyChange = ({ value: keyValue }) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onValueChange = ({ value }) => {
|
||||||
|
// TODO: Validate here or validate at a lower level component
|
||||||
|
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
keyValue,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemovePress = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
onRemove
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onRemove(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
this.props.onFocus();
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.props.onBlur();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
keyValue,
|
||||||
|
value,
|
||||||
|
keyPlaceholder,
|
||||||
|
valuePlaceholder,
|
||||||
|
isNew
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemContainer}>
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.keyInput}
|
||||||
|
name="key"
|
||||||
|
value={keyValue}
|
||||||
|
placeholder={keyPlaceholder}
|
||||||
|
onChange={this.onKeyChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.valueInput}
|
||||||
|
name="value"
|
||||||
|
value={value}
|
||||||
|
placeholder={valuePlaceholder}
|
||||||
|
onChange={this.onValueChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonWrapper}>
|
||||||
|
{
|
||||||
|
isNew ?
|
||||||
|
null :
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
tabIndex={-1}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValueListInputItem.propTypes = {
|
||||||
|
index: PropTypes.number,
|
||||||
|
keyValue: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
keyPlaceholder: PropTypes.string.isRequired,
|
||||||
|
valuePlaceholder: PropTypes.string.isRequired,
|
||||||
|
isNew: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
onFocus: PropTypes.func.isRequired,
|
||||||
|
onBlur: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyValueListInputItem.defaultProps = {
|
||||||
|
keyPlaceholder: 'Key',
|
||||||
|
valuePlaceholder: 'Value'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyValueListInputItem;
|
|
@ -1,89 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './KeyValueListInputItem.css';
|
|
||||||
|
|
||||||
interface KeyValueListInputItemProps {
|
|
||||||
index: number;
|
|
||||||
keyValue: string;
|
|
||||||
value: string;
|
|
||||||
keyPlaceholder?: string;
|
|
||||||
valuePlaceholder?: string;
|
|
||||||
isNew: boolean;
|
|
||||||
onChange: (index: number, itemValue: { key: string; value: string }) => void;
|
|
||||||
onRemove: (index: number) => void;
|
|
||||||
onFocus: () => void;
|
|
||||||
onBlur: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function KeyValueListInputItem({
|
|
||||||
index,
|
|
||||||
keyValue,
|
|
||||||
value,
|
|
||||||
keyPlaceholder = 'Key',
|
|
||||||
valuePlaceholder = 'Value',
|
|
||||||
isNew,
|
|
||||||
onChange,
|
|
||||||
onRemove,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
}: KeyValueListInputItemProps): JSX.Element {
|
|
||||||
const handleKeyChange = useCallback(
|
|
||||||
({ value: keyValue }: { value: string }) => {
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
},
|
|
||||||
[index, value, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleValueChange = useCallback(
|
|
||||||
({ value }: { value: string }) => {
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
},
|
|
||||||
[index, keyValue, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemovePress = useCallback(() => {
|
|
||||||
onRemove(index);
|
|
||||||
}, [index, onRemove]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.itemContainer}>
|
|
||||||
<div className={styles.keyInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.keyInput}
|
|
||||||
name="key"
|
|
||||||
value={keyValue}
|
|
||||||
placeholder={keyPlaceholder}
|
|
||||||
onChange={handleKeyChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.valueInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.valueInput}
|
|
||||||
name="value"
|
|
||||||
value={value}
|
|
||||||
placeholder={valuePlaceholder}
|
|
||||||
onChange={handleValueChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonWrapper}>
|
|
||||||
{isNew ? null : (
|
|
||||||
<IconButton
|
|
||||||
name={icons.REMOVE}
|
|
||||||
tabIndex={-1}
|
|
||||||
onPress={handleRemovePress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyValueListInputItem;
|
|
|
@ -5,13 +5,13 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { metadataProfileNames } from 'Helpers/Props';
|
import { metadataProfileNames } from 'Helpers/Props';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
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.metadataProfiles', sortByProp('name')),
|
createSortedSectionSelector('settings.metadataProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
|
@ -38,7 +38,7 @@ function createMapStateToProps() {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
isDisabled: includeNoChangeDisabled
|
disabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ function createMapStateToProps() {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
isDisabled: true
|
disabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,15 +18,15 @@ function MonitorAlbumsSelectInput(props) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
isDisabled: includeNoChangeDisabled
|
disabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeMixed) {
|
if (includeMixed) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: `(${translate('Mixed')})`,
|
value: '(Mixed)',
|
||||||
isDisabled: true
|
disabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ function MonitorNewItemsSelectInput(props) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
isDisabled: includeNoChangeDisabled
|
disabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ function MonitorNewItemsSelectInput(props) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
isDisabled: true
|
disabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
frontend/src/Components/Form/PasswordInput.css
Normal file
5
frontend/src/Components/Form/PasswordInput.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.input {
|
||||||
|
composes: input from '~Components/Form/TextInput.css';
|
||||||
|
|
||||||
|
font-family: $passwordFamily;
|
||||||
|
}
|
|
@ -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 {
|
||||||
'modalBody': string;
|
'input': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
|
@ -1,5 +1,7 @@
|
||||||
|
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) {
|
||||||
|
@ -11,14 +13,17 @@ function PasswordInput(props) {
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
{...props}
|
{...props}
|
||||||
type="password"
|
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
...TextInput.props
|
className: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
PasswordInput.defaultProps = {
|
||||||
|
className: styles.input
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordInput;
|
export default PasswordInput;
|
||||||
|
|
|
@ -14,8 +14,6 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||||
return inputTypes.CHECK;
|
return inputTypes.CHECK;
|
||||||
case 'device':
|
case 'device':
|
||||||
return inputTypes.DEVICE;
|
return inputTypes.DEVICE;
|
||||||
case 'keyValueList':
|
|
||||||
return inputTypes.KEY_VALUE_LIST;
|
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
return inputTypes.PLAYLIST;
|
return inputTypes.PLAYLIST;
|
||||||
case 'password':
|
case 'password':
|
||||||
|
@ -31,8 +29,6 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||||
return inputTypes.DYNAMIC_SELECT;
|
return inputTypes.DYNAMIC_SELECT;
|
||||||
}
|
}
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
case 'artistTag':
|
|
||||||
return inputTypes.ARTIST_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 sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
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', sortByProp('name')),
|
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
|
@ -26,7 +26,7 @@ function createMapStateToProps() {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
isDisabled: includeNoChangeDisabled
|
disabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ function createMapStateToProps() {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
isDisabled: true
|
disabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,6 @@ class SelectInput extends Component {
|
||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
value: optionValue,
|
value: optionValue,
|
||||||
isDisabled: optionIsDisabled = false,
|
|
||||||
...otherOptionProps
|
...otherOptionProps
|
||||||
} = option;
|
} = option;
|
||||||
|
|
||||||
|
@ -60,7 +59,6 @@ class SelectInput extends Component {
|
||||||
<option
|
<option
|
||||||
key={key}
|
key={key}
|
||||||
value={key}
|
value={key}
|
||||||
disabled={optionIsDisabled}
|
|
||||||
{...otherOptionProps}
|
{...otherOptionProps}
|
||||||
>
|
>
|
||||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue