mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 13:10:13 -07:00
Compare commits
277 commits
v2.5.2.431
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
d2330a3232 |
||
|
4cb306780f | ||
|
393db165f3 | ||
|
eb861f06d3 | ||
|
6f1b370772 |
||
|
074f06442a |
||
|
fef111d396 | ||
|
76b7713870 | ||
|
d50ed84541 | ||
|
002e8f5b69 | ||
|
c7b8aa8a04 | ||
|
91f06801ca |
||
|
dc61618711 | ||
|
fd00a5627c | ||
|
66ea1b1dfb | ||
|
72fa05cf41 | ||
|
c51b5c6fba | ||
|
efebab9ba2 | ||
|
47c32c9963 | ||
|
9f229bb684 | ||
|
f9b2e57696 | ||
|
4b48edab0a | ||
|
e087574de7 | ||
|
8877cf99f1 | ||
|
a56e5b3f9a | ||
|
5bb1949ea2 | ||
|
979042948d | ||
|
ebe59b18d9 | ||
|
086a451dff | ||
|
1bcb82eed0 | ||
|
ae9b4cec75 | ||
|
ed777de015 | ||
|
96f956a5d6 | ||
|
68a8f40746 | ||
|
c518cf63e7 |
||
|
da55b8578a | ||
|
234c29ef49 |
||
|
de169e8a1f | ||
|
4b300a448a | ||
|
785bcfda0b |
||
|
94ea751ad2 | ||
|
0c172b58f1 |
||
|
ea2ee70208 | ||
|
8b63928a25 |
||
|
7217e891f7 | ||
|
345bbcd992 |
||
|
bd9d7ba085 | ||
|
3937bebfea | ||
|
767b0930a5 | ||
|
c3f0fc640c | ||
|
9dbcc79436 | ||
|
3dd04cecbf | ||
|
d8850af019 | ||
|
fbfd24e226 |
||
|
d9562c701e | ||
|
d21ad2ad68 | ||
|
556f0ea54b | ||
|
e4a36ca388 | ||
|
1045684935 | ||
|
9ba71ae6b1 | ||
|
89b9352fef | ||
|
c83332e58c | ||
|
4677a1115a |
||
|
6150a57596 | ||
|
13f6b1a086 |
||
|
8027ab5d2e |
||
|
5bdc119b98 | ||
|
1b9b57ae9b | ||
|
c28a97cafd | ||
|
099d19a04d | ||
|
d381463b60 | ||
|
a86bd8e862 | ||
|
4bea38ab9c | ||
|
950c51bc59 | ||
|
18f13fe7f8 | ||
|
f8d4b3a59b | ||
|
5cf9624e55 |
||
|
81895f8033 | ||
|
a1c2bfa527 | ||
|
33049910de |
||
|
6dd87fd348 |
||
|
9314eb34ab | ||
|
84b91ba6c1 | ||
|
6c6f92fbed | ||
|
1e42ae94aa | ||
|
29f5810865 | ||
|
342c82aa1f | ||
|
5a3f879442 | ||
|
6e57c14e57 | ||
|
9fc549b43b | ||
|
a2201001c5 | ||
|
8c99280f07 | ||
|
07db508580 | ||
|
031f32a52c | ||
|
2997c16346 | ||
|
a1a53dbb5e | ||
|
e8bb78e5bb | ||
|
6292f223ac | ||
|
f4dc294ab3 | ||
|
23611cb116 |
||
|
f177345d01 | ||
|
ec050a7b3c | ||
|
860bd04c59 | ||
|
261f30d268 | ||
|
36998abba0 | ||
|
ad12617694 | ||
|
be115da157 | ||
|
664b972494 |
||
|
2b2fd5a175 | ||
|
d8222c066c |
||
|
bc6417229e | ||
|
e0e17a2ea7 | ||
|
5bf2ae9e6f |
||
|
8e01ba5f21 |
||
|
45e8ecffa0 | ||
|
3c4b438d27 | ||
|
8fd79d7291 | ||
|
477a799b8a | ||
|
51a38bc648 | ||
|
917f705695 | ||
|
5a1092b511 | ||
|
ef2c6366c4 | ||
|
1ffb82e364 |
||
|
e2f8753a6a | ||
|
739019498f | ||
|
396b2ae7c1 | ||
|
0216616738 | ||
|
82e0b628cc | ||
|
014f8a58b1 | ||
|
5cbb2848c7 |
||
|
554cf8ec55 | ||
|
4ff6c71456 | ||
|
7cfcf01ae3 | ||
|
17c5c66e54 |
||
|
40dab8deb9 | ||
|
39f0e4d989 | ||
|
35a46eca7b |
||
|
79b29f39f9 | ||
|
0e19c03e9a | ||
|
e6388cab94 | ||
|
47e504fbc9 | ||
|
1a40839202 | ||
|
25a80aa29d | ||
|
7255126af5 | ||
|
166f87ae68 | ||
|
babdf10273 |
||
|
19c2994ff3 | ||
|
e420ee0645 | ||
|
78469a96c9 | ||
|
bc6df548fc | ||
|
797e4c773e | ||
|
119141723a | ||
|
fd1719e58c | ||
|
41612708ff | ||
|
535caf1324 | ||
|
eb3c7d6990 | ||
|
4c603e24f6 | ||
|
ec93c33aa9 | ||
|
afb3fd5bd5 | ||
|
198a13755f | ||
|
44a5654918 | ||
|
8aa0754843 | ||
|
c42e96b55d | ||
|
f92935e3d2 | ||
|
13bb8f5089 | ||
|
ad084cdf91 | ||
|
4bcdc49777 | ||
|
502cb20898 | ||
|
0fd6c263b1 | ||
|
11af8a5e05 |
||
|
88196340a8 | ||
|
4048f2bd72 | ||
|
1f76f6cb19 | ||
|
cc409d50f5 | ||
|
14716a1405 | ||
|
f3a697ca68 | ||
|
f87a8fa9f5 | ||
|
b298bfd932 | ||
|
ecb7d9f6a6 | ||
|
eef55f65c6 | ||
|
beabad5e3a |
||
|
2b1684a793 | ||
|
f23d75d031 | ||
|
abe0090f94 | ||
|
8d32a532e4 | ||
|
a3b78aacdc | ||
|
c43a141b65 | ||
|
d2f5feab5d | ||
|
cfb517a90f | ||
|
3f81e0254f | ||
|
29d17c6347 | ||
|
23f7dc3d3c | ||
|
e39e990696 | ||
|
0c2ede48e8 | ||
|
ca23ac3011 | ||
|
e50e79167a | ||
|
fd3f493eb6 | ||
|
f6d3481e38 | ||
|
e04c28fe2d | ||
|
030300c896 | ||
|
50872f3629 | ||
|
3232e9ab94 | ||
|
8623b4410c | ||
|
a843a46fbe | ||
|
54a758a1b8 | ||
|
ca5379f817 | ||
|
96b51a02e2 | ||
|
f7acd57f73 | ||
|
20eb61dbc6 | ||
|
cb6608975e | ||
|
2db04a3452 | ||
|
5cead5f7ff | ||
|
850c08dda3 | ||
|
f005695b48 | ||
|
55626594c5 | ||
|
c645afc389 | ||
|
33d6169882 | ||
|
c750f4764f |
||
|
c6c52c4117 | ||
|
0e9a5fbd04 | ||
|
f3cd49a2da | ||
|
1a74118d6b | ||
|
9850823298 | ||
|
bd7d25f963 | ||
|
3a5012655e | ||
|
ba55a4778a | ||
|
ff91589f73 | ||
|
2caba01123 | ||
|
4588bc4a7e | ||
|
3381ffc311 |
||
|
f705603211 |
||
|
715274bcc7 | ||
|
9b063aa291 | ||
|
af4ff00476 | ||
|
b14c647c86 | ||
|
00cca22dc7 | ||
|
73fddd5201 | ||
|
b7e5a745a1 | ||
|
b67533bccf | ||
|
21d9ecccd6 | ||
|
34c9300cbf | ||
|
3e5af06622 | ||
|
ccce4f5cc0 |
||
|
5947b4642c | ||
|
856ac2ffa5 | ||
|
b2a4c75cce | ||
|
2818f4e073 | ||
|
60fe75877b | ||
|
2170ada8a2 | ||
|
c26c0d5bd6 | ||
|
d17c6a9b3e | ||
|
af6c0cc6f5 | ||
|
21344361e4 | ||
|
8efb602531 | ||
|
bd5f171fa9 | ||
|
1dee2aaf80 | ||
|
f024dad65f | ||
|
ef3b644d52 |
||
|
af4eeeb893 | ||
|
7738ee78c8 | ||
|
8d3dc0a470 | ||
|
2b8b8ed147 | ||
|
828e04bcad | ||
|
bb6528c104 | ||
|
f49388f3c4 | ||
|
8ff8c27e24 | ||
|
281bcb28fe | ||
|
e60a219671 | ||
|
34ac9dbfcb | ||
|
30ceb77615 | ||
|
304646f324 | ||
|
7555141961 | ||
|
df09e903d4 | ||
|
88f4c0c6cd | ||
|
8f9281f914 | ||
|
5fed16c38a | ||
|
963ffbea4e |
422 changed files with 11347 additions and 5770 deletions
|
@ -6,7 +6,7 @@
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"nodeGypDependencies": true,
|
"nodeGypDependencies": true,
|
||||||
"version": "16",
|
"version": "20",
|
||||||
"nvmVersion": "latest"
|
"nvmVersion": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -60,6 +60,7 @@ 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
|
||||||
|
|
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@v3
|
- uses: dessant/label-actions@v4
|
||||||
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@v4
|
- uses: dessant/lock-threads@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
|
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -121,6 +121,7 @@ _artifacts
|
||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
|
_temp*
|
||||||
*.Result.xml
|
*.Result.xml
|
||||||
coverage*.xml
|
coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
|
@ -139,12 +140,6 @@ 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
|
||||||
|
@ -163,34 +158,12 @@ 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/
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# 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)
|
||||||
|
@ -8,6 +9,9 @@
|
||||||
|
|
||||||
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.5.2'
|
majorVersion: '2.13.3'
|
||||||
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.424'
|
dotnetVersion: '6.0.427'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-22.04'
|
||||||
macImage: 'macOS-12'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
|
@ -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@2
|
- task: SonarCloudPrepare@3
|
||||||
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@2
|
- task: SonarCloudAnalyze@3
|
||||||
|
|
||||||
- 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@2
|
- task: SonarCloudPrepare@3
|
||||||
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: 'MSBuild'
|
scannerMode: 'dotnet'
|
||||||
projectKey: 'lidarr_Lidarr'
|
projectKey: 'lidarr_Lidarr'
|
||||||
projectName: 'Lidarr'
|
projectName: 'Lidarr'
|
||||||
projectVersion: '$(lidarrVersion)'
|
projectVersion: '$(lidarrVersion)'
|
||||||
|
@ -1226,10 +1226,10 @@ stages:
|
||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5
|
- task: reportgenerator@5.3.11
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
|
|
15
docs.sh
15
docs.sh
|
@ -1,13 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
ARCHITECTURE="${2:-x64}"
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-x64"
|
RUNTIME="win-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-x64"
|
RUNTIME="linux-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-x64"
|
RUNTIME="osx-$ARCHITECTURE"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v1 &
|
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|
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": true
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|
|
@ -26,6 +26,7 @@ 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
|
||||||
|
@ -134,6 +135,12 @@ 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')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -181,7 +188,7 @@ module.exports = (env) => {
|
||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.41'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,6 +16,7 @@ const mixinsFiles = [
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'autoprefixer',
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -172,7 +172,8 @@ function HistoryDetails(props) {
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message
|
message,
|
||||||
|
indexer
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -192,6 +193,14 @@ function HistoryDetails(props) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
indexer ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Indexer')}
|
||||||
|
data={indexer}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{
|
{
|
||||||
message ?
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|
|
@ -57,30 +57,40 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
iconName = icons.PAUSED;
|
iconName = icons.PAUSED;
|
||||||
title = 'Paused';
|
title = translate('Paused');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'queued') {
|
if (status === 'queued') {
|
||||||
iconName = icons.QUEUED;
|
iconName = icons.QUEUED;
|
||||||
title = 'Queued';
|
title = translate('Queued');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = 'Downloaded';
|
title = translate('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 += ' - Waiting to Import';
|
title += ` - ${translate('WaitingToImport')}`;
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
if (trackedDownloadState === 'importing') {
|
||||||
title += ' - Importing';
|
title += ` - ${translate('Importing')}`;
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
if (trackedDownloadState === 'failedPending') {
|
||||||
title += ' - Waiting to Process';
|
title += ` - ${translate('WaitingToProcess')}`;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,36 +101,38 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
title = 'Pending';
|
title = translate('Pending');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = 'Pending - Download client is unavailable';
|
title = translate('PendingDownloadClientUnavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'failed') {
|
if (status === 'failed') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = 'Download failed';
|
title = translate('DownloadFailed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
const warningMessage =
|
||||||
|
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 = `Import failed: ${sourceTitle}`;
|
title = translate('ImportFailed', { sourceTitle });
|
||||||
} else {
|
} else {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = 'Download failed';
|
title = translate('DownloadFailed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('AllAlbums')}
|
title={translate('AllAlbums')}
|
||||||
data="Monitor all new albums"
|
data={translate('MonitorAllAlbums')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('NewAlbums')}
|
title={translate('NewAlbums')}
|
||||||
data="Monitor new albums released after the newest existing album"
|
data={translate('MonitorNewAlbumsData')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('None')}
|
title={translate('None')}
|
||||||
data="Don't monitor any new albums"
|
data={translate('MonitorNoAlbumsData')}
|
||||||
/>
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface Album extends ModelBase {
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
|
lastSearchTime?: string;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,6 +121,8 @@
|
||||||
|
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.sizeOnDisk,
|
.sizeOnDisk,
|
||||||
|
.albumType,
|
||||||
|
.secondaryTypes,
|
||||||
.qualityProfileName,
|
.qualityProfileName,
|
||||||
.links,
|
.links,
|
||||||
.tags {
|
.tags {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
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;
|
||||||
|
@ -20,6 +21,7 @@ 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,6 +192,7 @@ class AlbumDetails extends Component {
|
||||||
duration,
|
duration,
|
||||||
overview,
|
overview,
|
||||||
albumType,
|
albumType,
|
||||||
|
secondaryTypes,
|
||||||
statistics = {},
|
statistics = {},
|
||||||
monitored,
|
monitored,
|
||||||
releaseDate,
|
releaseDate,
|
||||||
|
@ -204,6 +205,7 @@ class AlbumDetails extends Component {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
|
tracksError,
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
hasTrackFiles,
|
hasTrackFiles,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
|
@ -396,10 +398,11 @@ 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
|
||||||
|
@ -418,14 +421,15 @@ 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
|
||||||
|
@ -434,16 +438,15 @@ 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={
|
||||||
|
@ -459,32 +462,55 @@ 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>
|
||||||
</Label>
|
</div>
|
||||||
|
</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
|
||||||
|
@ -493,14 +519,15 @@ 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={
|
||||||
|
@ -526,8 +553,9 @@ class AlbumDetails extends Component {
|
||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !albumsError && !trackFilesError &&
|
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
||||||
<LoadingIndicator />
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -538,6 +566,14 @@ 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}>
|
||||||
|
@ -566,6 +602,14 @@ class AlbumDetails extends Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !media.length ?
|
||||||
|
<Alert kind={kinds.WARNING}>
|
||||||
|
{translate('NoMediumInformation')}
|
||||||
|
</Alert> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrganizePreviewModalConnector
|
<OrganizePreviewModalConnector
|
||||||
|
@ -632,6 +676,7 @@ 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,
|
||||||
|
@ -658,6 +703,8 @@ AlbumDetails.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
AlbumDetails.defaultProps = {
|
AlbumDetails.defaultProps = {
|
||||||
|
secondaryTypes: [],
|
||||||
|
statistics: {},
|
||||||
isSaving: false
|
isSaving: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import Updates from 'System/Updates/Updates';
|
||||||
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';
|
||||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={UpdatesConnector}
|
component={Updates}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -44,6 +44,7 @@ export interface CustomFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
|
version: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import AppSectionState, {
|
||||||
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';
|
||||||
|
@ -12,13 +13,16 @@ 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 { UiSettings } from 'typings/UiSettings';
|
import General from 'typings/Settings/General';
|
||||||
|
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,
|
||||||
|
@ -41,6 +45,11 @@ 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,
|
||||||
|
@ -50,7 +59,10 @@ export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<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;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import { AppSectionItemState } from './AppSectionState';
|
import Update from 'typings/Update';
|
||||||
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
|
updates: UpdateAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,9 +149,7 @@ class AlbumRow extends Component {
|
||||||
if (name === 'secondaryTypes') {
|
if (name === 'secondaryTypes') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{
|
{secondaryTypes.join(', ')}
|
||||||
secondaryTypes
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, tooltipPositions } from 'Helpers/Props';
|
import { icons, inputTypes, kinds, sizes, 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>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Monitored')}
|
{translate('Monitored')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -107,9 +107,10 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MonitorNewItems')}
|
{translate('MonitorNewItems')}
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -132,7 +133,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('QualityProfile')}
|
{translate('QualityProfile')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -146,10 +147,10 @@ class EditArtistModalContent extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
showMetadataProfile &&
|
showMetadataProfile ?
|
||||||
<FormGroup>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
Metadata Profile
|
{translate('MetadataProfile')}
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
|
@ -173,10 +174,11 @@ class EditArtistModalContent extends Component {
|
||||||
{...metadataProfileId}
|
{...metadataProfileId}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Path')}
|
{translate('Path')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -189,7 +191,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Tags')}
|
{translate('Tags')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -209,7 +211,7 @@ class EditArtistModalContent extends Component {
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={onDeleteArtistPress}
|
onPress={onDeleteArtistPress}
|
||||||
>
|
>
|
||||||
Delete
|
{translate('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/UiSettings';
|
import UiSettings from 'typings/Settings/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';
|
||||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
helpText={translate('TagsHelpText')}
|
helpText={translate('ICalTagsArtistHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
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.
|
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/lidarr/faq#why-cant-lidarr-see-my-files-on-a-remote-server' })} />
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
get name() {
|
get name() {
|
||||||
return translate('ImportFailed');
|
return translate('ImportCompleteFailed');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,6 +20,8 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
if ((/^botton/).test(data.placement)) {
|
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||||
data.styles.maxHeight = windowHeight - bottom;
|
|
||||||
} else {
|
|
||||||
data.styles.maxHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
|
||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true,
|
||||||
|
boundariesElement: 'viewport'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -49,12 +49,12 @@ function getComponent(type) {
|
||||||
case inputTypes.DEVICE:
|
case inputTypes.DEVICE:
|
||||||
return DeviceInputConnector;
|
return DeviceInputConnector;
|
||||||
|
|
||||||
case inputTypes.PLAYLIST:
|
|
||||||
return PlaylistInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.KEY_VALUE_LIST:
|
case inputTypes.KEY_VALUE_LIST:
|
||||||
return KeyValueListInput;
|
return KeyValueListInput;
|
||||||
|
|
||||||
|
case inputTypes.PLAYLIST:
|
||||||
|
return PlaylistInputConnector;
|
||||||
|
|
||||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||||
return MonitorAlbumsSelectInput;
|
return MonitorAlbumsSelectInput;
|
||||||
|
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
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;
|
|
104
frontend/src/Components/Form/KeyValueListInput.tsx
Normal file
104
frontend/src/Components/Form/KeyValueListInput.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
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,13 +5,19 @@
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputWrapper {
|
.keyInputWrapper {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +26,10 @@
|
||||||
.valueInput {
|
.valueInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: var(--inputBackgroundColor);
|
background-color: transparent;
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--helpTextColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
// 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;
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
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;
|
|
89
frontend/src/Components/Form/KeyValueListInputItem.tsx
Normal file
89
frontend/src/Components/Form/KeyValueListInputItem.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
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;
|
|
@ -14,6 +14,8 @@ 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':
|
||||||
|
|
|
@ -83,13 +83,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.modal.small,
|
|
||||||
.modal.medium {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
|
||||||
.modalContainer {
|
.modalContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
|
||||||
|
|
||||||
.isDisabled {
|
&.isDisabled {
|
||||||
color: var(--disabledColor);
|
color: var(--disabledColor);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
||||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||||
import styles from './PageHeader.css';
|
import styles from './PageHeader.css';
|
||||||
|
|
||||||
class PageHeader extends Component {
|
class PageHeader extends Component {
|
||||||
|
@ -83,6 +83,7 @@ class PageHeader extends Component {
|
||||||
size={14}
|
size={14}
|
||||||
title={translate('Donate')}
|
title={translate('Donate')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.translation}
|
className={styles.translation}
|
||||||
title={translate('SuggestTranslationChange')}
|
title={translate('SuggestTranslationChange')}
|
||||||
|
@ -90,7 +91,8 @@ class PageHeader extends Component {
|
||||||
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
<PageHeaderActionsMenuConnector
|
|
||||||
|
<PageHeaderActionsMenu
|
||||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Menu from 'Components/Menu/Menu';
|
|
||||||
import MenuButton from 'Components/Menu/MenuButton';
|
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import MenuItem from 'Components/Menu/MenuItem';
|
|
||||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './PageHeaderActionsMenu.css';
|
|
||||||
|
|
||||||
function PageHeaderActionsMenu(props) {
|
|
||||||
const {
|
|
||||||
formsAuth,
|
|
||||||
onKeyboardShortcutsPress,
|
|
||||||
onRestartPress,
|
|
||||||
onShutdownPress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Menu alignMenu={align.RIGHT}>
|
|
||||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
|
||||||
<Icon
|
|
||||||
name={icons.INTERACTIVE}
|
|
||||||
title={translate('Menu')}
|
|
||||||
/>
|
|
||||||
</MenuButton>
|
|
||||||
|
|
||||||
<MenuContent>
|
|
||||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.KEYBOARD}
|
|
||||||
/>
|
|
||||||
{translate('KeyboardShortcuts')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItemSeparator />
|
|
||||||
|
|
||||||
<MenuItem onPress={onRestartPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.RESTART}
|
|
||||||
/>
|
|
||||||
{translate('Restart')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem onPress={onShutdownPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.SHUTDOWN}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
/>
|
|
||||||
Shutdown
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
{
|
|
||||||
formsAuth &&
|
|
||||||
<div className={styles.separator} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
formsAuth &&
|
|
||||||
<MenuItem
|
|
||||||
to={`${window.Lidarr.urlBase}/logout`}
|
|
||||||
noRouter={true}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.LOGOUT}
|
|
||||||
/>
|
|
||||||
{translate('Logout')}
|
|
||||||
</MenuItem>
|
|
||||||
}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PageHeaderActionsMenu.propTypes = {
|
|
||||||
formsAuth: PropTypes.bool.isRequired,
|
|
||||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
|
||||||
onRestartPress: PropTypes.func.isRequired,
|
|
||||||
onShutdownPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageHeaderActionsMenu;
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Menu from 'Components/Menu/Menu';
|
||||||
|
import MenuButton from 'Components/Menu/MenuButton';
|
||||||
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
|
import MenuItem from 'Components/Menu/MenuItem';
|
||||||
|
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './PageHeaderActionsMenu.css';
|
||||||
|
|
||||||
|
interface PageHeaderActionsMenuProps {
|
||||||
|
onKeyboardShortcutsPress(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||||
|
const { onKeyboardShortcutsPress } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { authentication, isDocker } = useSelector(
|
||||||
|
(state: AppState) => state.system.status.item
|
||||||
|
);
|
||||||
|
|
||||||
|
const formsAuth = authentication === 'forms';
|
||||||
|
|
||||||
|
const handleRestartPress = useCallback(() => {
|
||||||
|
dispatch(restart());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleShutdownPress = useCallback(() => {
|
||||||
|
dispatch(shutdown());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Menu alignMenu={align.RIGHT}>
|
||||||
|
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||||
|
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<MenuContent>
|
||||||
|
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
||||||
|
{translate('KeyboardShortcuts')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{isDocker ? null : (
|
||||||
|
<>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
|
||||||
|
<MenuItem onPress={handleRestartPress}>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
||||||
|
{translate('Restart')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onPress={handleShutdownPress}>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.SHUTDOWN}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
/>
|
||||||
|
{translate('Shutdown')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formsAuth ? (
|
||||||
|
<>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
|
||||||
|
<MenuItem to={`${window.Lidarr.urlBase}/logout`} noRouter={true}>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
||||||
|
{translate('Logout')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeaderActionsMenu;
|
|
@ -1,56 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
|
||||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.system.status,
|
|
||||||
(status) => {
|
|
||||||
return {
|
|
||||||
formsAuth: status.item.authentication === 'forms'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
restart,
|
|
||||||
shutdown
|
|
||||||
};
|
|
||||||
|
|
||||||
class PageHeaderActionsMenuConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRestartPress = () => {
|
|
||||||
this.props.restart();
|
|
||||||
};
|
|
||||||
|
|
||||||
onShutdownPress = () => {
|
|
||||||
this.props.shutdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageHeaderActionsMenu
|
|
||||||
{...this.props}
|
|
||||||
onRestartPress={this.onRestartPress}
|
|
||||||
onShutdownPress={this.onShutdownPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PageHeaderActionsMenuConnector.propTypes = {
|
|
||||||
restart: PropTypes.func.isRequired,
|
|
||||||
shutdown: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
|
|
@ -224,10 +224,58 @@ class SignalRConnector extends Component {
|
||||||
repopulatePage('trackFileUpdated');
|
repopulatePage('trackFileUpdated');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleDownloadclient = ({ action, resource }) => {
|
||||||
|
const section = 'settings.downloadClients';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleHealth = () => {
|
handleHealth = () => {
|
||||||
this.props.dispatchFetchHealth();
|
this.props.dispatchFetchHealth();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleImportlist = ({ action, resource }) => {
|
||||||
|
const section = 'settings.importLists';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleIndexer = ({ action, resource }) => {
|
||||||
|
const section = 'settings.indexers';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMetadata = ({ action, resource }) => {
|
||||||
|
const section = 'settings.metadata';
|
||||||
|
|
||||||
|
if (action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleNotification = ({ action, resource }) => {
|
||||||
|
const section = 'settings.notifications';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleArtist = (body) => {
|
handleArtist = (body) => {
|
||||||
const action = body.action;
|
const action = body.action;
|
||||||
const section = 'artist';
|
const section = 'artist';
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
line-height: 1.52857143;
|
line-height: 1.52857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.pager {
|
.pager {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
|
|
||||||
<TileColor>#00ccff</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Lidarr",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "../../../../",
|
|
||||||
"theme_color": "#3a3f51",
|
|
||||||
"background_color": "#3a3f51",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
11
frontend/src/Content/browserconfig.xml
Normal file
11
frontend/src/Content/browserconfig.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="__URL_BASE__/Content/Images/Icons/mstile-150x150.png" />
|
||||||
|
<TileColor>
|
||||||
|
#00ccff
|
||||||
|
</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
19
frontend/src/Content/manifest.json
Normal file
19
frontend/src/Content/manifest.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "__INSTANCE_NAME__",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "__URL_BASE__/",
|
||||||
|
"theme_color": "#3a3f51",
|
||||||
|
"background_color": "#3a3f51",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ export const AUTO_COMPLETE = 'autoComplete';
|
||||||
export const CAPTCHA = 'captcha';
|
export const CAPTCHA = 'captcha';
|
||||||
export const CHECK = 'check';
|
export const CHECK = 'check';
|
||||||
export const DEVICE = 'device';
|
export const DEVICE = 'device';
|
||||||
export const PLAYLIST = 'playlist';
|
|
||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
|
export const PLAYLIST = 'playlist';
|
||||||
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
||||||
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
||||||
export const FLOAT = 'float';
|
export const FLOAT = 'float';
|
||||||
|
@ -34,8 +34,8 @@ export const all = [
|
||||||
CAPTCHA,
|
CAPTCHA,
|
||||||
CHECK,
|
CHECK,
|
||||||
DEVICE,
|
DEVICE,
|
||||||
PLAYLIST,
|
|
||||||
KEY_VALUE_LIST,
|
KEY_VALUE_LIST,
|
||||||
|
PLAYLIST,
|
||||||
MONITOR_ALBUMS_SELECT,
|
MONITOR_ALBUMS_SELECT,
|
||||||
MONITOR_NEW_ITEMS_SELECT,
|
MONITOR_NEW_ITEMS_SELECT,
|
||||||
FLOAT,
|
FLOAT,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Scroller from 'Components/Scroller/Scroller';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectAlbumRow from './SelectAlbumRow';
|
import SelectAlbumRow from './SelectAlbumRow';
|
||||||
import styles from './SelectAlbumModalContent.css';
|
import styles from './SelectAlbumModalContent.css';
|
||||||
|
@ -19,6 +20,7 @@ const columns = [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
label: () => translate('AlbumTitle'),
|
label: () => translate('AlbumTitle'),
|
||||||
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -29,6 +31,7 @@ const columns = [
|
||||||
{
|
{
|
||||||
name: 'releaseDate',
|
name: 'releaseDate',
|
||||||
label: () => translate('ReleaseDate'),
|
label: () => translate('ReleaseDate'),
|
||||||
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -63,16 +66,22 @@ class SelectAlbumModalContent extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items,
|
|
||||||
onAlbumSelect,
|
|
||||||
onModalClose,
|
|
||||||
isFetching,
|
isFetching,
|
||||||
...otherProps
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
onSortPress,
|
||||||
|
onAlbumSelect,
|
||||||
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const filter = this.state.filter;
|
const filter = this.state.filter;
|
||||||
const filterLower = filter.toLowerCase();
|
const filterLower = filter.toLowerCase();
|
||||||
|
|
||||||
|
const errorMessage = getErrorMessage(error, 'Unable to load albums');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
|
@ -83,10 +92,14 @@ class SelectAlbumModalContent extends Component {
|
||||||
className={styles.modalBody}
|
className={styles.modalBody}
|
||||||
scrollDirection={scrollDirections.NONE}
|
scrollDirection={scrollDirections.NONE}
|
||||||
>
|
>
|
||||||
{
|
<Scroller
|
||||||
isFetching &&
|
className={styles.scroller}
|
||||||
<LoadingIndicator />
|
autoFocus={false}
|
||||||
}
|
>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
className={styles.filterInput}
|
className={styles.filterInput}
|
||||||
placeholder={translate('FilterAlbumPlaceholder')}
|
placeholder={translate('FilterAlbumPlaceholder')}
|
||||||
|
@ -96,14 +109,12 @@ class SelectAlbumModalContent extends Component {
|
||||||
onChange={this.onFilterChange}
|
onChange={this.onFilterChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Scroller
|
{isPopulated && !!items.length ? (
|
||||||
className={styles.scroller}
|
|
||||||
autoFocus={false}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
{...otherProps}
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{
|
{
|
||||||
|
@ -122,7 +133,7 @@ class SelectAlbumModalContent extends Component {
|
||||||
}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
}
|
) : null}
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
|
@ -137,8 +148,13 @@ class SelectAlbumModalContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectAlbumModalContent.propTypes = {
|
SelectAlbumModalContent.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.string,
|
||||||
|
onSortPress: PropTypes.func.isRequired,
|
||||||
onAlbumSelect: PropTypes.func.isRequired,
|
onAlbumSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,18 +3,14 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import {
|
import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions';
|
||||||
clearInteractiveImportAlbums,
|
import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||||
fetchInteractiveImportAlbums,
|
|
||||||
saveInteractiveImportItem,
|
|
||||||
setInteractiveImportAlbumsSort,
|
|
||||||
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createClientSideCollectionSelector('interactiveImport.albums'),
|
createClientSideCollectionSelector('albumSelection'),
|
||||||
(albums) => {
|
(albums) => {
|
||||||
return albums;
|
return albums;
|
||||||
}
|
}
|
||||||
|
@ -22,9 +18,9 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchInteractiveImportAlbums,
|
fetchAlbums,
|
||||||
setInteractiveImportAlbumsSort,
|
setAlbumsSort,
|
||||||
clearInteractiveImportAlbums,
|
clearAlbums,
|
||||||
updateInteractiveImportItem,
|
updateInteractiveImportItem,
|
||||||
saveInteractiveImportItem
|
saveInteractiveImportItem
|
||||||
};
|
};
|
||||||
|
@ -39,20 +35,20 @@ class SelectAlbumModalContentConnector extends Component {
|
||||||
artistId
|
artistId
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
this.props.fetchInteractiveImportAlbums({ artistId });
|
this.props.fetchAlbums({ artistId });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// This clears the albums for the queue and hides the queue
|
// This clears the albums for the queue and hides the queue
|
||||||
// We'll need another place to store albums for manual import
|
// We'll need another place to store albums for manual import
|
||||||
this.props.clearInteractiveImportAlbums();
|
this.props.clearAlbums();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onSortPress = (sortKey, sortDirection) => {
|
onSortPress = (sortKey, sortDirection) => {
|
||||||
this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
|
this.props.setAlbumsSort({ sortKey, sortDirection });
|
||||||
};
|
};
|
||||||
|
|
||||||
onAlbumSelect = (albumId) => {
|
onAlbumSelect = (albumId) => {
|
||||||
|
@ -82,6 +78,7 @@ class SelectAlbumModalContentConnector extends Component {
|
||||||
return (
|
return (
|
||||||
<SelectAlbumModalContent
|
<SelectAlbumModalContent
|
||||||
{...this.props}
|
{...this.props}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
onAlbumSelect={this.onAlbumSelect}
|
onAlbumSelect={this.onAlbumSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -92,9 +89,9 @@ SelectAlbumModalContentConnector.propTypes = {
|
||||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
artistId: PropTypes.number.isRequired,
|
artistId: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
fetchInteractiveImportAlbums: PropTypes.func.isRequired,
|
fetchAlbums: PropTypes.func.isRequired,
|
||||||
setInteractiveImportAlbumsSort: PropTypes.func.isRequired,
|
setAlbumsSort: PropTypes.func.isRequired,
|
||||||
clearInteractiveImportAlbums: PropTypes.func.isRequired,
|
clearAlbums: PropTypes.func.isRequired,
|
||||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
@ -130,7 +131,8 @@ class AddNewItem extends Component {
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
<div>{getErrorMessage(error)}</div>
|
|
||||||
|
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
@ -8,6 +8,7 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||||
|
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
|
||||||
|
|
||||||
function CustomFormatSettingsPage() {
|
function CustomFormatSettingsPage() {
|
||||||
return (
|
return (
|
||||||
|
@ -17,11 +18,13 @@ function CustomFormatSettingsPage() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
showSave={false}
|
showSave={false}
|
||||||
additionalButtons={
|
additionalButtons={
|
||||||
<Fragment>
|
<>
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<ParseToolbarButton />
|
<ParseToolbarButton />
|
||||||
</Fragment>
|
|
||||||
|
<ManageCustomFormatsToolbarButton />
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||||
|
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
||||||
|
|
||||||
function mapStateToProps() {
|
function mapStateToProps() {
|
||||||
return {};
|
return {};
|
||||||
|
@ -36,6 +37,7 @@ class EditCustomFormatModalConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
EditCustomFormatModalConnector.propTypes = {
|
EditCustomFormatModalConnector.propTypes = {
|
||||||
|
...EditCustomFormatModalContentConnector.propTypes,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageCustomFormatsEditModalContent from './ManageCustomFormatsEditModalContent';
|
||||||
|
|
||||||
|
interface ManageCustomFormatsEditModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
customFormatIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageCustomFormatsEditModal(
|
||||||
|
props: ManageCustomFormatsEditModalProps
|
||||||
|
) {
|
||||||
|
const { isOpen, customFormatIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageCustomFormatsEditModalContent
|
||||||
|
customFormatIds={customFormatIds}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageCustomFormatsEditModal;
|
|
@ -0,0 +1,16 @@
|
||||||
|
.modalFooter {
|
||||||
|
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointExtraSmall) {
|
||||||
|
.modalFooter {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'modalFooter': string;
|
||||||
|
'selected': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,125 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ManageCustomFormatsEditModalContent.css';
|
||||||
|
|
||||||
|
interface SavePayload {
|
||||||
|
includeCustomFormatWhenRenaming?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageCustomFormatsEditModalContentProps {
|
||||||
|
customFormatIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
const enableOptions = [
|
||||||
|
{
|
||||||
|
key: NO_CHANGE,
|
||||||
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Enabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Disabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function ManageCustomFormatsEditModalContent(
|
||||||
|
props: ManageCustomFormatsEditModalContentProps
|
||||||
|
) {
|
||||||
|
const { customFormatIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
const [includeCustomFormatWhenRenaming, setIncludeCustomFormatWhenRenaming] =
|
||||||
|
useState(NO_CHANGE);
|
||||||
|
|
||||||
|
const save = useCallback(() => {
|
||||||
|
let hasChanges = false;
|
||||||
|
const payload: SavePayload = {};
|
||||||
|
|
||||||
|
if (includeCustomFormatWhenRenaming !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.includeCustomFormatWhenRenaming =
|
||||||
|
includeCustomFormatWhenRenaming === 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
onSavePress(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [includeCustomFormatWhenRenaming, onSavePress, onModalClose]);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ name, value }: { name: string; value: string }) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'includeCustomFormatWhenRenaming':
|
||||||
|
setIncludeCustomFormatWhenRenaming(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
`EditCustomFormatsModalContent Unknown Input: '${name}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCount = customFormatIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('EditSelectedCustomFormats')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeCustomFormatWhenRenaming')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="includeCustomFormatWhenRenaming"
|
||||||
|
value={includeCustomFormatWhenRenaming}
|
||||||
|
values={enableOptions}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div className={styles.selected}>
|
||||||
|
{translate('CountCustomFormatsSelected', {
|
||||||
|
count: selectedCount,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageCustomFormatsEditModalContent;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageCustomFormatsModalContent from './ManageCustomFormatsModalContent';
|
||||||
|
|
||||||
|
interface ManageCustomFormatsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageCustomFormatsModal(props: ManageCustomFormatsModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageCustomFormatsModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageCustomFormatsModal;
|
|
@ -0,0 +1,16 @@
|
||||||
|
.leftButtons,
|
||||||
|
.rightButtons {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 50%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightButtons {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'deleteButton': string;
|
||||||
|
'leftButtons': string;
|
||||||
|
'rightButtons': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,244 @@
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { CustomFormatAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
bulkDeleteCustomFormats,
|
||||||
|
bulkEditCustomFormats,
|
||||||
|
setManageCustomFormatsSort,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import ManageCustomFormatsEditModal from './Edit/ManageCustomFormatsEditModal';
|
||||||
|
import ManageCustomFormatsModalRow from './ManageCustomFormatsModalRow';
|
||||||
|
import styles from './ManageCustomFormatsModalContent.css';
|
||||||
|
|
||||||
|
// TODO: This feels janky to do, but not sure of a better way currently
|
||||||
|
type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
|
typeof ManageCustomFormatsModalRow
|
||||||
|
>['onSelectedChange'];
|
||||||
|
|
||||||
|
const COLUMNS: Column[] = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'includeCustomFormatWhenRenaming',
|
||||||
|
label: () => translate('IncludeCustomFormatWhenRenaming'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: '',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ManageCustomFormatsModalContentProps {
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageCustomFormatsModalContent(
|
||||||
|
props: ManageCustomFormatsModalContentProps
|
||||||
|
) {
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
isDeleting,
|
||||||
|
isSaving,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
}: CustomFormatAppState = useSelector(
|
||||||
|
createClientSideCollectionSelector('settings.customFormats')
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds: number[] = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
dispatch(setManageCustomFormatsSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDeletePress = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}, [setIsDeleteModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteModalClose = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}, [setIsDeleteModalOpen]);
|
||||||
|
|
||||||
|
const onEditPress = useCallback(() => {
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onEditModalClose = useCallback(() => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onConfirmDelete = useCallback(() => {
|
||||||
|
dispatch(bulkDeleteCustomFormats({ ids: selectedIds }));
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}, [selectedIds, dispatch]);
|
||||||
|
|
||||||
|
const onSavePress = useCallback(
|
||||||
|
(payload: object) => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
bulkEditCustomFormats({
|
||||||
|
ids: selectedIds,
|
||||||
|
...payload,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectAllChange = useCallback(
|
||||||
|
({ value }: SelectStateInputProps) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
|
||||||
|
({ id, value, shiftKey = false }) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorMessage = getErrorMessage(error, 'Unable to load custom formats.');
|
||||||
|
const anySelected = selectedCount > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('ManageCustomFormats')}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoCustomFormatsFound')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
|
<Table
|
||||||
|
columns={COLUMNS}
|
||||||
|
horizontalScroll={true}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<ManageCustomFormatsModalRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
{...item}
|
||||||
|
columns={COLUMNS}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<div className={styles.leftButtons}>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onDeletePress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onEditPress}
|
||||||
|
>
|
||||||
|
{translate('Edit')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<ManageCustomFormatsEditModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
customFormatIds={selectedIds}
|
||||||
|
onModalClose={onEditModalClose}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteSelectedCustomFormats')}
|
||||||
|
message={translate('DeleteSelectedCustomFormatsMessageText', {
|
||||||
|
count: selectedIds.length,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={onConfirmDelete}
|
||||||
|
onCancel={onDeleteModalClose}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageCustomFormatsModalContent;
|
|
@ -0,0 +1,12 @@
|
||||||
|
.name,
|
||||||
|
.includeCustomFormatWhenRenaming {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 40px;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'actions': string;
|
||||||
|
'includeCustomFormatWhenRenaming': string;
|
||||||
|
'name': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,126 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { deleteCustomFormat } from 'Store/Actions/settingsActions';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector';
|
||||||
|
import styles from './ManageCustomFormatsModalRow.css';
|
||||||
|
|
||||||
|
interface ManageCustomFormatsModalRowProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
includeCustomFormatWhenRenaming: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
isSelected?: boolean;
|
||||||
|
onSelectedChange(result: SelectStateInputProps): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDeletingSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.customFormats.isDeleting,
|
||||||
|
(isDeleting) => {
|
||||||
|
return isDeleting;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isSelected,
|
||||||
|
name,
|
||||||
|
includeCustomFormatWhenRenaming,
|
||||||
|
onSelectedChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isDeleting = useSelector(isDeletingSelector());
|
||||||
|
|
||||||
|
const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handlelectedChange = useCallback(
|
||||||
|
(result: SelectStateInputProps) => {
|
||||||
|
onSelectedChange({
|
||||||
|
...result,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onSelectedChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEditCustomFormatModalOpen = useCallback(() => {
|
||||||
|
setIsEditCustomFormatModalOpen(true);
|
||||||
|
}, [setIsEditCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleEditCustomFormatModalClose = useCallback(() => {
|
||||||
|
setIsEditCustomFormatModalOpen(false);
|
||||||
|
}, [setIsEditCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteCustomFormatPress = useCallback(() => {
|
||||||
|
setIsEditCustomFormatModalOpen(false);
|
||||||
|
setIsDeleteCustomFormatModalOpen(true);
|
||||||
|
}, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteCustomFormatModalClose = useCallback(() => {
|
||||||
|
setIsDeleteCustomFormatModalOpen(false);
|
||||||
|
}, [setIsDeleteCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleConfirmDeleteCustomFormat = useCallback(() => {
|
||||||
|
dispatch(deleteCustomFormat({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={handlelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
|
||||||
|
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
onPress={handleEditCustomFormatModalOpen}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<EditCustomFormatModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditCustomFormatModalOpen}
|
||||||
|
onModalClose={handleEditCustomFormatModalClose}
|
||||||
|
onDeleteCustomFormatPress={handleDeleteCustomFormatPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteCustomFormatModalOpen}
|
||||||
|
kind="danger"
|
||||||
|
title={translate('DeleteCustomFormat')}
|
||||||
|
message={translate('DeleteCustomFormatMessageText', { name })}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
onConfirm={handleConfirmDeleteCustomFormat}
|
||||||
|
onCancel={handleDeleteCustomFormatModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageCustomFormatsModalRow;
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import ManageCustomFormatsModal from './ManageCustomFormatsModal';
|
||||||
|
|
||||||
|
function ManageCustomFormatsToolbarButton() {
|
||||||
|
const [isManageModalOpen, openManageModal, closeManageModal] =
|
||||||
|
useModalOpenState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('ManageFormats')}
|
||||||
|
iconName={icons.MANAGE}
|
||||||
|
onPress={openManageModal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ManageCustomFormatsModal
|
||||||
|
isOpen={isManageModalOpen}
|
||||||
|
onModalClose={closeManageModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageCustomFormatsToolbarButton;
|
|
@ -7,8 +7,8 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
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';
|
||||||
|
@ -52,12 +52,13 @@ function EditSpecificationModalContent(props) {
|
||||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
<div>
|
<div>
|
||||||
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
{'More details'} <Link to="https://www.regular-expressions.info/tutorial.html">{'Here'}</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{'Regular expressions can be tested '}
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||||
<Link to="http://regexstorm.net/tester">Here</Link>
|
</div>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
|
||||||
import {
|
import {
|
||||||
bulkDeleteDownloadClients,
|
bulkDeleteDownloadClients,
|
||||||
bulkEditDownloadClients,
|
bulkEditDownloadClients,
|
||||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
typeof ManageDownloadClientsModalRow
|
typeof ManageDownloadClientsModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
|
@ -82,8 +82,6 @@ const COLUMNS = [
|
||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
sortKey?: string;
|
|
||||||
sortDirection?: SortDirection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
|
@ -220,9 +218,9 @@ function ManageDownloadClientsModalContent(
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
{isPopulated && !error && !items.length && (
|
{isPopulated && !error && !items.length ? (
|
||||||
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -156,6 +156,7 @@ class GeneralSettings extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoggingSettings
|
<LoggingSettings
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onInputChange={onInputChange}
|
onInputChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -15,12 +15,14 @@ const logLevelOptions = [
|
||||||
|
|
||||||
function LoggingSettings(props) {
|
function LoggingSettings(props) {
|
||||||
const {
|
const {
|
||||||
|
advancedSettings,
|
||||||
settings,
|
settings,
|
||||||
onInputChange
|
onInputChange
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
logLevel
|
logLevel,
|
||||||
|
logSizeLimit
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -39,11 +41,30 @@ function LoggingSettings(props) {
|
||||||
{...logLevel}
|
{...logLevel}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('LogSizeLimit')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="logSizeLimit"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
unit="MB"
|
||||||
|
helpText={translate('LogSizeLimitHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...logSizeLimit}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoggingSettings.propTypes = {
|
LoggingSettings.propTypes = {
|
||||||
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
settings: PropTypes.object.isRequired,
|
settings: PropTypes.object.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -292,7 +292,7 @@ function EditImportListModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('TagsHelpText')}
|
helpText={translate('ImportListTagsHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -198,9 +198,9 @@ function ManageImportListsModalContent(
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
{isPopulated && !error && !items.length && (
|
{isPopulated && !error && !items.length ? (
|
||||||
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
|
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
|
||||||
import {
|
import {
|
||||||
bulkDeleteIndexers,
|
bulkDeleteIndexers,
|
||||||
bulkEditIndexers,
|
bulkEditIndexers,
|
||||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
typeof ManageIndexersModalRow
|
typeof ManageIndexersModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
|
@ -82,8 +82,6 @@ const COLUMNS = [
|
||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
sortKey?: string;
|
|
||||||
sortDirection?: SortDirection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
@ -215,9 +213,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
{isPopulated && !error && !items.length && (
|
{isPopulated && !error && !items.length ? (
|
||||||
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
|
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -191,26 +191,21 @@ class MediaManagement extends Component {
|
||||||
<FieldSet
|
<FieldSet
|
||||||
legend={translate('Importing')}
|
legend={translate('Importing')}
|
||||||
>
|
>
|
||||||
{
|
|
||||||
!isWindows &&
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
size={sizes.MEDIUM}
|
size={sizes.MEDIUM}
|
||||||
>
|
>
|
||||||
<FormLabel>
|
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||||
{translate('SkipFreeSpaceCheck')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipFreeSpaceCheckWhenImporting"
|
name="skipFreeSpaceCheckWhenImporting"
|
||||||
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -15,11 +14,11 @@ function createMapStateToProps() {
|
||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
(state) => state.settings.namingExamples,
|
(state) => state.settings.namingExamples,
|
||||||
createSettingsSectionSelector(SECTION),
|
createSettingsSectionSelector(SECTION),
|
||||||
(advancedSettings, examples, sectionSettings) => {
|
(advancedSettings, namingExamples, sectionSettings) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
examples: examples.item,
|
examples: namingExamples.item,
|
||||||
examplesPopulated: !_.isEmpty(examples.item),
|
examplesPopulated: namingExamples.isPopulated,
|
||||||
...sectionSettings
|
...sectionSettings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,9 +94,9 @@ class RootFolder extends Component {
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={this.state.isDeleteRootFolderModalOpen}
|
isOpen={this.state.isDeleteRootFolderModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteRootFolder')}
|
title={translate('RemoveRootFolder')}
|
||||||
message={translate('DeleteRootFolderMessageText', { name })}
|
message={translate('RemoveRootFolderArtistsMessageText', { name })}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Remove')}
|
||||||
onConfirm={this.onConfirmDeleteRootFolder}
|
onConfirm={this.onConfirmDeleteRootFolder}
|
||||||
onCancel={this.onDeleteRootFolderModalClose}
|
onCancel={this.onDeleteRootFolderModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -105,7 +105,7 @@ function EditNotificationModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('TagsHelpText')}
|
helpText={translate('NotificationsTagsArtistHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error ?
|
!isFetching && !!error ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToAddANewQualityProfilePleaseTryAgain')}
|
{translate('AddDelayProfileError')}
|
||||||
</div> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
{
|
{
|
||||||
id === 1 ?
|
id === 1 ?
|
||||||
<Alert>
|
<Alert>
|
||||||
{translate('DefaultDelayProfileHelpText')}
|
{translate('DefaultDelayProfileArtist')}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
{...tags}
|
{...tags}
|
||||||
helpText={translate('TagsHelpText')}
|
helpText={translate('DelayProfileArtistTagsHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('TagsHelpText')}
|
helpText={translate('ReleaseProfileTagArtistHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar {
|
.track {
|
||||||
top: 9px;
|
top: 9px;
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle {
|
.thumb {
|
||||||
top: 1px;
|
top: 1px;
|
||||||
z-index: 0 !important;
|
z-index: 0 !important;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// 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 {
|
||||||
'bar': string;
|
|
||||||
'handle': string;
|
|
||||||
'kilobitsPerSecond': string;
|
'kilobitsPerSecond': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
'qualityDefinition': string;
|
'qualityDefinition': string;
|
||||||
|
@ -10,7 +8,9 @@ interface CssExports {
|
||||||
'sizeLimit': string;
|
'sizeLimit': string;
|
||||||
'sizes': string;
|
'sizes': string;
|
||||||
'slider': string;
|
'slider': string;
|
||||||
|
'thumb': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
'track': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -55,6 +55,27 @@ class QualityDefinition extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
trackRenderer(props, state) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={styles.track}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbRenderer(props, state) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={styles.thumb}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
@ -174,6 +195,7 @@ class QualityDefinition extends Component {
|
||||||
|
|
||||||
<div className={styles.sizeLimit}>
|
<div className={styles.sizeLimit}>
|
||||||
<ReactSlider
|
<ReactSlider
|
||||||
|
className={styles.slider}
|
||||||
min={slider.min}
|
min={slider.min}
|
||||||
max={slider.max}
|
max={slider.max}
|
||||||
step={slider.step}
|
step={slider.step}
|
||||||
|
@ -182,9 +204,9 @@ class QualityDefinition extends Component {
|
||||||
withTracks={true}
|
withTracks={true}
|
||||||
allowCross={false}
|
allowCross={false}
|
||||||
snapDragDisabled={true}
|
snapDragDisabled={true}
|
||||||
className={styles.slider}
|
pearling={true}
|
||||||
trackClassName={styles.bar}
|
renderThumb={this.thumbRenderer}
|
||||||
thumbClassName={styles.handle}
|
renderTrack={this.trackRenderer}
|
||||||
onChange={this.onSliderChange}
|
onChange={this.onSliderChange}
|
||||||
onAfterChange={this.onAfterSliderChange}
|
onAfterChange={this.onAfterSliderChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
|
||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,13 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||||
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import Tags from './Tags';
|
import Tags from './Tags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.tags,
|
createSortedSectionSelector('tags', sortByProp('label')),
|
||||||
(tags) => {
|
(tags) => {
|
||||||
const isFetching = tags.isFetching || tags.details.isFetching;
|
const isFetching = tags.isFetching || tags.details.isFetching;
|
||||||
const error = tags.error || tags.details.error;
|
const error = tags.error || tags.details.error;
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
|
import createSetClientSideCollectionSortReducer
|
||||||
|
from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
@ -21,6 +26,9 @@ export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
|
||||||
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
|
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
|
||||||
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
||||||
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
||||||
|
export const BULK_EDIT_CUSTOM_FORMATS = 'settings/downloadClients/bulkEditCustomFormats';
|
||||||
|
export const BULK_DELETE_CUSTOM_FORMATS = 'settings/downloadClients/bulkDeleteCustomFormats';
|
||||||
|
export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManageCustomFormatsSort';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -28,6 +36,9 @@ export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
||||||
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
|
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
|
||||||
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
|
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
|
||||||
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
|
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
|
||||||
|
export const bulkEditCustomFormats = createThunk(BULK_EDIT_CUSTOM_FORMATS);
|
||||||
|
export const bulkDeleteCustomFormats = createThunk(BULK_DELETE_CUSTOM_FORMATS);
|
||||||
|
export const setManageCustomFormatsSort = createAction(SET_MANAGE_CUSTOM_FORMATS_SORT);
|
||||||
|
|
||||||
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
|
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
|
@ -47,20 +58,30 @@ export default {
|
||||||
// State
|
// State
|
||||||
|
|
||||||
defaultState: {
|
defaultState: {
|
||||||
isSchemaFetching: false,
|
|
||||||
isSchemaPopulated: false,
|
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {},
|
||||||
|
|
||||||
|
isSchemaFetching: false,
|
||||||
|
isSchemaPopulated: false,
|
||||||
|
schemaError: null,
|
||||||
schema: {
|
schema: {
|
||||||
includeCustomFormatWhenRenaming: false
|
includeCustomFormatWhenRenaming: false
|
||||||
},
|
},
|
||||||
error: null,
|
|
||||||
isDeleting: false,
|
sortKey: 'name',
|
||||||
deleteError: null,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
isSaving: false,
|
sortPredicates: {
|
||||||
saveError: null,
|
name: ({ name }) => {
|
||||||
items: [],
|
return name.toLocaleLowerCase();
|
||||||
pendingChanges: {}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -82,7 +103,10 @@ export default {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
|
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'),
|
||||||
|
[BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk')
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -102,7 +126,9 @@ export default {
|
||||||
newState.pendingChanges = pendingChanges;
|
newState.pendingChanges = pendingChanges;
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
return updateSectionState(state, section, newState);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -96,8 +96,8 @@ export default {
|
||||||
sortKey: 'name',
|
sortKey: 'name',
|
||||||
sortDirection: sortDirections.ASCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
name: function(item) {
|
name: ({ name }) => {
|
||||||
return item.name.toLowerCase();
|
return name.toLocaleLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -100,8 +100,8 @@ export default {
|
||||||
sortKey: 'name',
|
sortKey: 'name',
|
||||||
sortDirection: sortDirections.ASCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
name: function(item) {
|
name: ({ name }) => {
|
||||||
return item.name.toLowerCase();
|
return name.toLocaleLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
86
frontend/src/Store/Actions/albumSelectionActions.js
Normal file
86
frontend/src/Store/Actions/albumSelectionActions.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'albumSelection';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isFetching: false,
|
||||||
|
isReprocessing: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
sortKey: 'title',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
items: [],
|
||||||
|
sortPredicates: {
|
||||||
|
title: ({ title }) => {
|
||||||
|
return title.toLocaleLowerCase();
|
||||||
|
},
|
||||||
|
|
||||||
|
releaseDate: function({ releaseDate }, direction) {
|
||||||
|
if (releaseDate) {
|
||||||
|
return moment(releaseDate).unix();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === sortDirections.DESCENDING) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number.MAX_VALUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistState = [
|
||||||
|
'albumSelection.sortKey',
|
||||||
|
'albumSelection.sortDirection'
|
||||||
|
];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_ALBUMS = 'albumSelection/fetchAlbums';
|
||||||
|
export const SET_ALBUMS_SORT = 'albumSelection/setAlbumsSort';
|
||||||
|
export const CLEAR_ALBUMS = 'albumSelection/clearAlbums';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchAlbums = createThunk(FETCH_ALBUMS);
|
||||||
|
export const setAlbumsSort = createAction(SET_ALBUMS_SORT);
|
||||||
|
export const clearAlbums = createAction(CLEAR_ALBUMS);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
[FETCH_ALBUMS]: createFetchHandler(section, '/album')
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[SET_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||||
|
|
||||||
|
[CLEAR_ALBUMS]: (state) => {
|
||||||
|
return updateSectionState(state, section, {
|
||||||
|
...defaultState,
|
||||||
|
sortKey: state.sortKey,
|
||||||
|
sortDirection: state.sortDirection
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}, defaultState, section);
|
|
@ -151,7 +151,7 @@ export const defaultState = {
|
||||||
{
|
{
|
||||||
name: 'genres',
|
name: 'genres',
|
||||||
label: () => translate('Genres'),
|
label: () => translate('Genres'),
|
||||||
isSortable: false,
|
isSortable: true,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -150,7 +150,7 @@ export const defaultState = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'importFailed',
|
key: 'importFailed',
|
||||||
label: () => translate('ImportFailed'),
|
label: () => translate('ImportCompleteFailed'),
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'eventType',
|
key: 'eventType',
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as albums from './albumActions';
|
import * as albums from './albumActions';
|
||||||
import * as albumHistory from './albumHistoryActions';
|
import * as albumHistory from './albumHistoryActions';
|
||||||
|
import * as albumSelection from './albumSelectionActions';
|
||||||
import * as app from './appActions';
|
import * as app from './appActions';
|
||||||
import * as artist from './artistActions';
|
import * as artist from './artistActions';
|
||||||
import * as artistHistory from './artistHistoryActions';
|
import * as artistHistory from './artistHistoryActions';
|
||||||
|
@ -29,14 +30,18 @@ import * as wanted from './wantedActions';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
app,
|
app,
|
||||||
|
albums,
|
||||||
|
albumHistory,
|
||||||
|
albumSelection,
|
||||||
|
artist,
|
||||||
|
artistHistory,
|
||||||
|
artistIndex,
|
||||||
blocklist,
|
blocklist,
|
||||||
captcha,
|
captcha,
|
||||||
calendar,
|
calendar,
|
||||||
commands,
|
commands,
|
||||||
customFilters,
|
customFilters,
|
||||||
albums,
|
|
||||||
trackFiles,
|
trackFiles,
|
||||||
albumHistory,
|
|
||||||
history,
|
history,
|
||||||
interactiveImportActions,
|
interactiveImportActions,
|
||||||
oAuth,
|
oAuth,
|
||||||
|
@ -47,9 +52,6 @@ export default [
|
||||||
providerOptions,
|
providerOptions,
|
||||||
queue,
|
queue,
|
||||||
releases,
|
releases,
|
||||||
artist,
|
|
||||||
artistHistory,
|
|
||||||
artistIndex,
|
|
||||||
search,
|
search,
|
||||||
settings,
|
settings,
|
||||||
system,
|
system,
|
||||||
|
|
|
@ -16,7 +16,6 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create
|
||||||
|
|
||||||
export const section = 'interactiveImport';
|
export const section = 'interactiveImport';
|
||||||
|
|
||||||
const albumsSection = `${section}.albums`;
|
|
||||||
const trackFilesSection = `${section}.trackFiles`;
|
const trackFilesSection = `${section}.trackFiles`;
|
||||||
let abortCurrentFetchRequest = null;
|
let abortCurrentFetchRequest = null;
|
||||||
let abortCurrentRequest = null;
|
let abortCurrentRequest = null;
|
||||||
|
@ -58,15 +57,6 @@ export const defaultState = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
albums: {
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
sortKey: 'albumTitle',
|
|
||||||
sortDirection: sortDirections.ASCENDING,
|
|
||||||
items: []
|
|
||||||
},
|
|
||||||
|
|
||||||
trackFiles: {
|
trackFiles: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
|
@ -97,10 +87,6 @@ export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
|
||||||
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
||||||
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
||||||
|
|
||||||
export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/fetchInteractiveImportAlbums';
|
|
||||||
export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'interactiveImport/clearInteractiveImportAlbumsSort';
|
|
||||||
export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/clearInteractiveImportAlbums';
|
|
||||||
|
|
||||||
export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles';
|
export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles';
|
||||||
export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles';
|
export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles';
|
||||||
|
|
||||||
|
@ -117,10 +103,6 @@ export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
|
||||||
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
||||||
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
||||||
|
|
||||||
export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT_ALBUMS);
|
|
||||||
export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT);
|
|
||||||
export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS);
|
|
||||||
|
|
||||||
export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES);
|
export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES);
|
||||||
export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES);
|
export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES);
|
||||||
|
|
||||||
|
@ -253,8 +235,6 @@ export const actionHandlers = handleThunks({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'),
|
|
||||||
|
|
||||||
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
|
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -336,14 +316,6 @@ export const reducers = createHandleActions({
|
||||||
return Object.assign({}, state, { importMode: payload.importMode });
|
return Object.assign({}, state, { importMode: payload.importMode });
|
||||||
},
|
},
|
||||||
|
|
||||||
[SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(albumsSection),
|
|
||||||
|
|
||||||
[CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => {
|
|
||||||
return updateSectionState(state, albumsSection, {
|
|
||||||
...defaultState.albums
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => {
|
[CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => {
|
||||||
return updateSectionState(state, trackFilesSection, {
|
return updateSectionState(state, trackFilesSection, {
|
||||||
...defaultState.trackFiles
|
...defaultState.trackFiles
|
||||||
|
|
|
@ -52,6 +52,12 @@ export const defaultState = {
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'albums.lastSearchTime',
|
||||||
|
label: () => translate('LastSearched'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// name: 'status',
|
// name: 'status',
|
||||||
// label: 'Status',
|
// label: 'Status',
|
||||||
|
@ -131,6 +137,12 @@ export const defaultState = {
|
||||||
// label: 'Status',
|
// label: 'Status',
|
||||||
// isVisible: true
|
// isVisible: true
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
name: 'albums.lastSearchTime',
|
||||||
|
label: () => translate('LastSearched'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
columnLabel: () => translate('Actions'),
|
columnLabel: () => translate('Actions'),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import AlbumAppState from 'App/State/AlbumAppState';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import { createArtistSelectorForHook } from './createArtistSelector';
|
import { createArtistSelectorForHook } from './createArtistSelector';
|
||||||
|
@ -7,7 +8,7 @@ function createArtistAlbumsSelector(artistId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.albums,
|
(state: AppState) => state.albums,
|
||||||
createArtistSelectorForHook(artistId),
|
createArtistSelectorForHook(artistId),
|
||||||
(albums, artist = {} as Artist) => {
|
(albums: AlbumAppState, artist = {} as Artist) => {
|
||||||
const { isFetching, isPopulated, error, items } = albums;
|
const { isFetching, isPopulated, error, items } = albums;
|
||||||
|
|
||||||
const filteredAlbums = items.filter(
|
const filteredAlbums = items.filter(
|
||||||
|
|
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