mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-06 04:52:21 -07:00
Compare commits
291 commits
v2.5.0.427
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
b514de2840 | ||
|
6673d14edd | ||
|
da954dc31e | ||
|
51d22bed2a | ||
|
04e0d3f22e | ||
|
809db4022e | ||
|
afbead8a88 | ||
|
950e94564e | ||
|
790e1233ec | ||
|
b259136d3f | ||
|
54607eb2e0 | ||
|
07b3ebb1aa | ||
|
1ec1fc4c67 | ||
|
36e67fdc2e | ||
|
916fbb2a69 | ||
|
4904e52c95 | ||
|
49f452a958 | ||
|
6ef3a0a6d4 | ||
|
683f690a85 | ||
|
0216f46b37 | ||
|
d2bd9b4849 | ||
|
70e524409c | ||
|
3a3c19fb77 | ||
|
c6362cb47a | ||
|
cb7e3987a9 |
444 changed files with 11045 additions and 5793 deletions
|
@ -6,7 +6,7 @@
|
|||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"version": "20",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
|
|
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -60,6 +60,7 @@ body:
|
|||
- Master
|
||||
- Develop
|
||||
- Nightly
|
||||
- Plugins (experimental)
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
required: true
|
||||
|
|
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
|
@ -12,6 +12,6 @@ jobs:
|
|||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v3
|
||||
- uses: dessant/label-actions@v4
|
||||
with:
|
||||
process-only: 'issues'
|
||||
|
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
|
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -121,6 +121,7 @@ _artifacts
|
|||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
|
@ -139,12 +140,6 @@ project.fragment.lock.json
|
|||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
#VS outout folders
|
||||
bin
|
||||
obj
|
||||
output/*
|
||||
|
||||
|
||||
# macOS metadata files
|
||||
._*
|
||||
.DS_Store
|
||||
|
@ -163,34 +158,12 @@ Thumbs.db
|
|||
/tools/Addins/*
|
||||
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
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||
.idea/
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Lidarr
|
||||
|
||||
[](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)
|
||||

|
||||
[](#backers)
|
||||
|
|
|
@ -9,18 +9,18 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.5.0'
|
||||
majorVersion: '2.13.1'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.424'
|
||||
dotnetVersion: '6.0.427'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-12'
|
||||
linuxImage: 'ubuntu-22.04'
|
||||
macImage: 'macOS-13'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
|
@ -1120,19 +1120,19 @@ stages:
|
|||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@3
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'lidarr'
|
||||
scannerMode: 'CLI'
|
||||
scannerMode: 'cli'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'lidarr_Lidarr.UI'
|
||||
cliProjectName: 'LidarrUI'
|
||||
cliProjectVersion: '$(lidarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@2
|
||||
- task: SonarCloudAnalyze@3
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
|
@ -1208,12 +1208,12 @@ stages:
|
|||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'lidarr'
|
||||
scannerMode: 'MSBuild'
|
||||
scannerMode: 'dotnet'
|
||||
projectKey: 'lidarr_Lidarr'
|
||||
projectName: 'Lidarr'
|
||||
projectVersion: '$(lidarrVersion)'
|
||||
|
@ -1226,10 +1226,10 @@ stages:
|
|||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@2
|
||||
- task: SonarCloudAnalyze@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5
|
||||
- task: reportgenerator@5.3.11
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
|
|
|
@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
|
|||
app_guid=${app_guid:-media}
|
||||
|
||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||
|
||||
# Create User / Group as needed
|
||||
|
@ -114,7 +114,7 @@ case "$ARCH" in
|
|||
esac
|
||||
echo ""
|
||||
echo "Removing previous tarballs"
|
||||
# -f to Force so we fail if it doesnt exist
|
||||
# -f to Force so we fail if it doesn't exist
|
||||
rm -f "${app^}".*.tar.gz
|
||||
echo ""
|
||||
echo "Downloading..."
|
||||
|
|
15
docs.sh
15
docs.sh
|
@ -1,13 +1,18 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
ARCHITECTURE="${2:-x64}"
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
RUNTIME="win-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
RUNTIME="linux-x64"
|
||||
RUNTIME="linux-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
RUNTIME="osx-x64"
|
||||
RUNTIME="osx-$ARCHITECTURE"
|
||||
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
|
||||
fi
|
||||
|
||||
|
@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
|||
dotnet new tool-manifest
|
||||
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
|
||||
|
||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
|||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
|
|
@ -26,6 +26,7 @@ module.exports = (env) => {
|
|||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
|
@ -67,7 +68,7 @@ module.exports = (env) => {
|
|||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name]-[contenthash].js',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
|
@ -92,7 +93,7 @@ module.exports = (env) => {
|
|||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
|
@ -134,6 +135,12 @@ module.exports = (env) => {
|
|||
{
|
||||
source: 'frontend/src/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,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
corejs: '3.41'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -202,7 +209,7 @@ module.exports = (env) => {
|
|||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -16,6 +16,7 @@ const mixinsFiles = [
|
|||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
|
|
@ -172,7 +172,8 @@ function HistoryDetails(props) {
|
|||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message
|
||||
message,
|
||||
indexer
|
||||
} = data;
|
||||
|
||||
return (
|
||||
|
@ -192,6 +193,14 @@ function HistoryDetails(props) {
|
|||
null
|
||||
}
|
||||
|
||||
{
|
||||
indexer ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{
|
||||
message ?
|
||||
<DescriptionListItem
|
||||
|
|
|
@ -26,4 +26,5 @@
|
|||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -57,30 +57,40 @@ function QueueStatusCell(props) {
|
|||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
title = translate('Paused');
|
||||
}
|
||||
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
title = translate('Queued');
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
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') {
|
||||
title += ' - Waiting to Import';
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ' - Importing';
|
||||
title += ` - ${translate('Importing')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ' - Waiting to Process';
|
||||
title += ` - ${translate('WaitingToProcess')}`;
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
@ -91,36 +101,38 @@ function QueueStatusCell(props) {
|
|||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
title = translate('Pending');
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = 'Pending - Download client is unavailable';
|
||||
title = translate('PendingDownloadClientUnavailable');
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
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 (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
title = translate('ImportFailed', { sourceTitle });
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ interface Album extends ModelBase {
|
|||
monitored: boolean;
|
||||
releaseDate: string;
|
||||
statistics: Statistics;
|
||||
lastSearchTime?: string;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -121,6 +121,8 @@
|
|||
|
||||
.releaseDate,
|
||||
.sizeOnDisk,
|
||||
.albumType,
|
||||
.secondaryTypes,
|
||||
.qualityProfileName,
|
||||
.links,
|
||||
.tags {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
interface CssExports {
|
||||
'albumNavigationButton': string;
|
||||
'albumNavigationButtons': string;
|
||||
'albumType': string;
|
||||
'alternateTitlesIconContainer': string;
|
||||
'backdrop': string;
|
||||
'backdropOverlay': string;
|
||||
|
@ -20,6 +21,7 @@ interface CssExports {
|
|||
'overview': string;
|
||||
'qualityProfileName': string;
|
||||
'releaseDate': string;
|
||||
'secondaryTypes': string;
|
||||
'sizeOnDisk': string;
|
||||
'tags': string;
|
||||
'title': string;
|
||||
|
|
|
@ -192,6 +192,7 @@ class AlbumDetails extends Component {
|
|||
duration,
|
||||
overview,
|
||||
albumType,
|
||||
secondaryTypes,
|
||||
statistics = {},
|
||||
monitored,
|
||||
releaseDate,
|
||||
|
@ -204,6 +205,7 @@ class AlbumDetails extends Component {
|
|||
isFetching,
|
||||
isPopulated,
|
||||
albumsError,
|
||||
tracksError,
|
||||
trackFilesError,
|
||||
hasTrackFiles,
|
||||
shortDateFormat,
|
||||
|
@ -396,10 +398,11 @@ class AlbumDetails extends Component {
|
|||
<div className={styles.details}>
|
||||
<div>
|
||||
{
|
||||
!!duration &&
|
||||
duration ?
|
||||
<span className={styles.duration}>
|
||||
{formatDuration(duration)}
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
<HeartRating
|
||||
|
@ -418,14 +421,15 @@ class AlbumDetails extends Component {
|
|||
title={translate('ReleaseDate')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.releaseDate}>
|
||||
{moment(releaseDate).format(shortDateFormat)}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
|
@ -434,16 +438,15 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -459,32 +462,55 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!albumType &&
|
||||
albumType ?
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('Type')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
<span className={styles.albumType}>
|
||||
{albumType}
|
||||
</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
|
||||
|
@ -493,14 +519,15 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -526,8 +553,9 @@ class AlbumDetails extends Component {
|
|||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !albumsError && !trackFilesError &&
|
||||
<LoadingIndicator />
|
||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -538,6 +566,14 @@ class AlbumDetails extends Component {
|
|||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && tracksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('TracksLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && trackFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
|
@ -566,6 +602,14 @@ class AlbumDetails extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !media.length ?
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('NoMediumInformation')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
|
@ -632,6 +676,7 @@ AlbumDetails.propTypes = {
|
|||
duration: PropTypes.number,
|
||||
overview: PropTypes.string,
|
||||
albumType: PropTypes.string.isRequired,
|
||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
|
@ -658,6 +703,8 @@ AlbumDetails.propTypes = {
|
|||
};
|
||||
|
||||
AlbumDetails.defaultProps = {
|
||||
secondaryTypes: [],
|
||||
statistics: {},
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={UpdatesConnector}
|
||||
component={Updates}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface CustomFilter {
|
|||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
version: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
|
|
|
@ -4,6 +4,7 @@ import AppSectionState, {
|
|||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
|
@ -12,13 +13,16 @@ import MetadataProfile from 'typings/MetadataProfile';
|
|||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
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
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
|
@ -41,6 +45,11 @@ export interface MetadataProfilesAppState
|
|||
extends AppSectionState<MetadataProfile>,
|
||||
AppSectionSchemaState<MetadataProfile> {}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface RootFolderAppState
|
||||
extends AppSectionState<RootFolder>,
|
||||
AppSectionDeleteState,
|
||||
|
@ -50,7 +59,10 @@ export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
|||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
customFormats: CustomFormatAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
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 UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
|
|
|
@ -149,9 +149,7 @@ class AlbumRow extends Component {
|
|||
if (name === 'secondaryTypes') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
secondaryTypes
|
||||
}
|
||||
{secondaryTypes.join(', ')}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
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 styles from './EditArtistModalContent.css';
|
||||
|
||||
|
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
|
|||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('Monitored')}
|
||||
</FormLabel>
|
||||
|
@ -107,9 +107,10 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewItems')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
|
@ -132,7 +133,7 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('QualityProfile')}
|
||||
</FormLabel>
|
||||
|
@ -146,10 +147,10 @@ class EditArtistModalContent extends Component {
|
|||
</FormGroup>
|
||||
|
||||
{
|
||||
showMetadataProfile &&
|
||||
<FormGroup>
|
||||
showMetadataProfile ?
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
Metadata Profile
|
||||
{translate('MetadataProfile')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
|
@ -173,10 +174,11 @@ class EditArtistModalContent extends Component {
|
|||
{...metadataProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('Path')}
|
||||
</FormLabel>
|
||||
|
@ -189,7 +191,7 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('Tags')}
|
||||
</FormLabel>
|
||||
|
@ -209,7 +211,7 @@ class EditArtistModalContent extends Component {
|
|||
kind={kinds.DANGER}
|
||||
onPress={onDeleteArtistPress}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
|||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
|||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
value={tags}
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ICalTagsArtistHelpText')}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -3,8 +3,8 @@ import React, { Component } from 'react';
|
|||
import Alert from 'Components/Alert';
|
||||
import PathInput from 'Components/Form/PathInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
|
|||
className={styles.mappedDrivesWarning}
|
||||
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>
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
|||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
|
||||
import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue';
|
||||
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
import styles from './FilterBuilderRow.css';
|
||||
|
||||
|
@ -68,7 +68,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.METADATA_PROFILE:
|
||||
return MetadataProfileFilterBuilderRowValueConnector;
|
||||
return MetadataProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
||||
return MonitorNewItemsFilterBuilderRowValue;
|
||||
|
@ -80,7 +80,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||
return QualityFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValueConnector;
|
||||
return QualityProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.ARTIST:
|
||||
return ArtistFilterBuilderRowValue;
|
||||
|
|
|
@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
|
|||
{
|
||||
id: 7,
|
||||
get name() {
|
||||
return translate('ImportFailed');
|
||||
return translate('ImportCompleteFailed');
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMetadataProfilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.metadataProfiles.items,
|
||||
(metadataProfiles) => {
|
||||
return metadataProfiles;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function MetadataProfileFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
const metadataProfiles = useSelector(createMetadataProfilesSelector());
|
||||
|
||||
const tagList = metadataProfiles
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
.sort(sortByProp('name'));
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default MetadataProfileFilterBuilderRowValue;
|
|
@ -1,28 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(metadataProfiles) => {
|
||||
const tagList = metadataProfiles.items.map((metadataProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = metadataProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createQualityProfilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function QualityProfileFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
||||
|
||||
const tagList = qualityProfiles
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
.sort(sortByProp('name'));
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default QualityProfileFilterBuilderRowValue;
|
|
@ -1,28 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = qualityProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|||
import TextInput from './TextInput';
|
||||
import styles from './EnhancedSelectInput.css';
|
||||
|
||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||
|
||||
function isArrowKey(keyCode) {
|
||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||
}
|
||||
|
@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
|||
// Listeners
|
||||
|
||||
onComputeMaxHeight = (data) => {
|
||||
const {
|
||||
top,
|
||||
bottom
|
||||
} = data.offsets.reference;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if ((/^botton/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom;
|
||||
} else {
|
||||
data.styles.maxHeight = top;
|
||||
}
|
||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
|
|||
order: 851,
|
||||
enabled: true,
|
||||
fn: this.onComputeMaxHeight
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -49,12 +49,12 @@ function getComponent(type) {
|
|||
case inputTypes.DEVICE:
|
||||
return DeviceInputConnector;
|
||||
|
||||
case inputTypes.PLAYLIST:
|
||||
return PlaylistInputConnector;
|
||||
|
||||
case inputTypes.KEY_VALUE_LIST:
|
||||
return KeyValueListInput;
|
||||
|
||||
case inputTypes.PLAYLIST:
|
||||
return PlaylistInputConnector;
|
||||
|
||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||
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 {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
.keyInputWrapper {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.valueInputWrapper {
|
||||
flex: 1 0 0;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
flex: 0 0 22px;
|
||||
}
|
||||
|
@ -20,6 +26,10 @@
|
|||
.valueInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
background-color: transparent;
|
||||
color: var(--textColor);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--helpTextColor);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'buttonWrapper': string;
|
||||
'inputWrapper': string;
|
||||
'itemContainer': string;
|
||||
'keyInput': string;
|
||||
'keyInputWrapper': string;
|
||||
'valueInput': string;
|
||||
'valueInputWrapper': string;
|
||||
}
|
||||
export const cssExports: 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;
|
||||
case 'device':
|
||||
return inputTypes.DEVICE;
|
||||
case 'keyValueList':
|
||||
return inputTypes.KEY_VALUE_LIST;
|
||||
case 'playlist':
|
||||
return inputTypes.PLAYLIST;
|
||||
case 'password':
|
||||
|
|
|
@ -83,13 +83,6 @@
|
|||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.modal.small,
|
||||
.modal.medium {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalContainer {
|
||||
position: fixed;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
&.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
|||
import translate from 'Utilities/String/translate';
|
||||
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
class PageHeader extends Component {
|
||||
|
@ -83,6 +83,7 @@ class PageHeader extends Component {
|
|||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.translation}
|
||||
title={translate('SuggestTranslationChange')}
|
||||
|
@ -90,7 +91,8 @@ class PageHeader extends Component {
|
|||
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
||||
size={24}
|
||||
/>
|
||||
<PageHeaderActionsMenuConnector
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</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');
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
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) => {
|
||||
const action = body.action;
|
||||
const section = 'artist';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
line-height: 1.52857143;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.tableContainer {
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.headerCell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
height: 25px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.pager {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.headerCell {
|
||||
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 CHECK = 'check';
|
||||
export const DEVICE = 'device';
|
||||
export const PLAYLIST = 'playlist';
|
||||
export const KEY_VALUE_LIST = 'keyValueList';
|
||||
export const PLAYLIST = 'playlist';
|
||||
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
||||
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
||||
export const FLOAT = 'float';
|
||||
|
@ -34,8 +34,8 @@ export const all = [
|
|||
CAPTCHA,
|
||||
CHECK,
|
||||
DEVICE,
|
||||
PLAYLIST,
|
||||
KEY_VALUE_LIST,
|
||||
PLAYLIST,
|
||||
MONITOR_ALBUMS_SELECT,
|
||||
MONITOR_NEW_ITEMS_SELECT,
|
||||
FLOAT,
|
||||
|
|
|
@ -11,6 +11,7 @@ import Scroller from 'Components/Scroller/Scroller';
|
|||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectAlbumRow from './SelectAlbumRow';
|
||||
import styles from './SelectAlbumModalContent.css';
|
||||
|
@ -19,6 +20,7 @@ const columns = [
|
|||
{
|
||||
name: 'title',
|
||||
label: () => translate('AlbumTitle'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
@ -29,6 +31,7 @@ const columns = [
|
|||
{
|
||||
name: 'releaseDate',
|
||||
label: () => translate('ReleaseDate'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
@ -63,16 +66,22 @@ class SelectAlbumModalContent extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
onAlbumSelect,
|
||||
onModalClose,
|
||||
isFetching,
|
||||
...otherProps
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
onSortPress,
|
||||
onAlbumSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load albums');
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
@ -83,10 +92,14 @@ class SelectAlbumModalContent extends Component {
|
|||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterAlbumPlaceholder')}
|
||||
|
@ -96,14 +109,12 @@ class SelectAlbumModalContent extends Component {
|
|||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
>
|
||||
{
|
||||
{isPopulated && !!items.length ? (
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
|
@ -122,7 +133,7 @@ class SelectAlbumModalContent extends Component {
|
|||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
) : null}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
|
@ -137,8 +148,13 @@ class SelectAlbumModalContent extends Component {
|
|||
}
|
||||
|
||||
SelectAlbumModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).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,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -3,18 +3,14 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
clearInteractiveImportAlbums,
|
||||
fetchInteractiveImportAlbums,
|
||||
saveInteractiveImportItem,
|
||||
setInteractiveImportAlbumsSort,
|
||||
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions';
|
||||
import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('interactiveImport.albums'),
|
||||
createClientSideCollectionSelector('albumSelection'),
|
||||
(albums) => {
|
||||
return albums;
|
||||
}
|
||||
|
@ -22,9 +18,9 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchInteractiveImportAlbums,
|
||||
setInteractiveImportAlbumsSort,
|
||||
clearInteractiveImportAlbums,
|
||||
fetchAlbums,
|
||||
setAlbumsSort,
|
||||
clearAlbums,
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
};
|
||||
|
@ -39,20 +35,20 @@ class SelectAlbumModalContentConnector extends Component {
|
|||
artistId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchInteractiveImportAlbums({ artistId });
|
||||
this.props.fetchAlbums({ artistId });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// This clears the albums for the queue and hides the queue
|
||||
// We'll need another place to store albums for manual import
|
||||
this.props.clearInteractiveImportAlbums();
|
||||
this.props.clearAlbums();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
|
||||
this.props.setAlbumsSort({ sortKey, sortDirection });
|
||||
};
|
||||
|
||||
onAlbumSelect = (albumId) => {
|
||||
|
@ -82,6 +78,7 @@ class SelectAlbumModalContentConnector extends Component {
|
|||
return (
|
||||
<SelectAlbumModalContent
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onAlbumSelect={this.onAlbumSelect}
|
||||
/>
|
||||
);
|
||||
|
@ -92,9 +89,9 @@ SelectAlbumModalContentConnector.propTypes = {
|
|||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
artistId: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||
setInteractiveImportAlbumsSort: PropTypes.func.isRequired,
|
||||
clearInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||
fetchAlbums: PropTypes.func.isRequired,
|
||||
setAlbumsSort: PropTypes.func.isRequired,
|
||||
clearAlbums: PropTypes.func.isRequired,
|
||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
|
|
@ -18,12 +18,17 @@
|
|||
.leftButtons,
|
||||
.rightButtons {
|
||||
display: flex;
|
||||
flex: 1 0 50%;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.leftButtons {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
justify-content: flex-end;
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
|
||||
.importMode,
|
||||
|
@ -31,6 +36,7 @@
|
|||
composes: select from '~Components/Form/SelectInput.css';
|
||||
|
||||
margin-right: 10px;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
@ -43,10 +49,12 @@
|
|||
.leftButtons,
|
||||
.rightButtons {
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.leftButtons {
|
||||
align-items: flex-start;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
|
@ -130,7 +131,8 @@ class AddNewItem extends Component {
|
|||
<div className={styles.helpText}>
|
||||
{translate('FailedLoadingSearchResults')}
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
|
||||
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
@ -8,6 +8,7 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
|||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
|
||||
|
||||
function CustomFormatSettingsPage() {
|
||||
return (
|
||||
|
@ -17,11 +18,13 @@ function CustomFormatSettingsPage() {
|
|||
// @ts-ignore
|
||||
showSave={false}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<ParseToolbarButton />
|
||||
</Fragment>
|
||||
|
||||
<ManageCustomFormatsToolbarButton />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
|
@ -36,6 +37,7 @@ class EditCustomFormatModalConnector extends Component {
|
|||
}
|
||||
|
||||
EditCustomFormatModalConnector.propTypes = {
|
||||
...EditCustomFormatModalContentConnector.propTypes,
|
||||
onModalClose: 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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
@ -52,12 +52,13 @@ function EditSpecificationModalContent(props) {
|
|||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<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>' }} />
|
||||
{'More details'} <Link to="https://www.regular-expressions.info/tutorial.html">{'Here'}</Link>
|
||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||
</div>
|
||||
<div>
|
||||
{'Regular expressions can be tested '}
|
||||
<Link to="http://regexstorm.net/tester">Here</Link>
|
||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
|
|
@ -10,11 +10,11 @@ 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 SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteDownloadClients,
|
||||
bulkEditDownloadClients,
|
||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||
typeof ManageDownloadClientsModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
|
@ -82,8 +82,6 @@ const COLUMNS = [
|
|||
|
||||
interface ManageDownloadClientsModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageDownloadClientsModalContent(
|
||||
|
@ -220,9 +218,9 @@ function ManageDownloadClientsModalContent(
|
|||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
{isPopulated && !error && !items.length && (
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||
<Table
|
||||
|
|
|
@ -156,6 +156,7 @@ class GeneralSettings extends Component {
|
|||
/>
|
||||
|
||||
<LoggingSettings
|
||||
advancedSettings={advancedSettings}
|
||||
settings={settings}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -15,12 +15,14 @@ const logLevelOptions = [
|
|||
|
||||
function LoggingSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
logLevel
|
||||
logLevel,
|
||||
logSizeLimit
|
||||
} = settings;
|
||||
|
||||
return (
|
||||
|
@ -39,11 +41,30 @@ function LoggingSettings(props) {
|
|||
{...logLevel}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
LoggingSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -292,7 +292,7 @@ function EditImportListModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ImportListTagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -198,9 +198,9 @@ function ManageImportListsModalContent(
|
|||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
{isPopulated && !error && !items.length && (
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||
<Table
|
||||
|
|
|
@ -10,11 +10,11 @@ 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 SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteIndexers,
|
||||
bulkEditIndexers,
|
||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||
typeof ManageIndexersModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
|
@ -82,8 +82,6 @@ const COLUMNS = [
|
|||
|
||||
interface ManageIndexersModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
|
@ -215,9 +213,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
{isPopulated && !error && !items.length && (
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||
<Table
|
||||
|
|
|
@ -191,26 +191,21 @@ class MediaManagement extends Component {
|
|||
<FieldSet
|
||||
legend={translate('Importing')}
|
||||
>
|
||||
{
|
||||
!isWindows &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>
|
||||
{translate('SkipFreeSpaceCheck')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -15,11 +14,11 @@ function createMapStateToProps() {
|
|||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.namingExamples,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, examples, sectionSettings) => {
|
||||
(advancedSettings, namingExamples, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
examples: examples.item,
|
||||
examplesPopulated: !_.isEmpty(examples.item),
|
||||
examples: namingExamples.item,
|
||||
examplesPopulated: namingExamples.isPopulated,
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
|
|
|
@ -94,9 +94,9 @@ class RootFolder extends Component {
|
|||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteRootFolderModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteRootFolder')}
|
||||
message={translate('DeleteRootFolderMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
title={translate('RemoveRootFolder')}
|
||||
message={translate('RemoveRootFolderArtistsMessageText', { name })}
|
||||
confirmLabel={translate('Remove')}
|
||||
onConfirm={this.onConfirmDeleteRootFolder}
|
||||
onCancel={this.onDeleteRootFolderModalClose}
|
||||
/>
|
||||
|
|
|
@ -105,7 +105,7 @@ function EditNotificationModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('NotificationsTagsArtistHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
|
|||
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<div>
|
||||
{translate('UnableToAddANewQualityProfilePleaseTryAgain')}
|
||||
</div> :
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddDelayProfileError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
|
|||
{
|
||||
id === 1 ?
|
||||
<Alert>
|
||||
{translate('DefaultDelayProfileHelpText')}
|
||||
{translate('DefaultDelayProfileArtist')}
|
||||
</Alert> :
|
||||
|
||||
<FormGroup>
|
||||
|
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
|
|||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...tags}
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('DelayProfileArtistTagsHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ReleaseProfileTagArtistHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
height: 20px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
.track {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
.thumb {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'bar': string;
|
||||
'handle': string;
|
||||
'kilobitsPerSecond': string;
|
||||
'quality': string;
|
||||
'qualityDefinition': string;
|
||||
|
@ -10,7 +8,9 @@ interface CssExports {
|
|||
'sizeLimit': string;
|
||||
'sizes': string;
|
||||
'slider': string;
|
||||
'thumb': string;
|
||||
'title': string;
|
||||
'track': string;
|
||||
}
|
||||
export const cssExports: 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
|
||||
|
||||
|
@ -174,6 +195,7 @@ class QualityDefinition extends Component {
|
|||
|
||||
<div className={styles.sizeLimit}>
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
|
@ -182,9 +204,9 @@ class QualityDefinition extends Component {
|
|||
withTracks={true}
|
||||
allowCross={false}
|
||||
snapDragDisabled={true}
|
||||
className={styles.slider}
|
||||
trackClassName={styles.bar}
|
||||
thumbClassName={styles.handle}
|
||||
pearling={true}
|
||||
renderThumb={this.thumbRenderer}
|
||||
renderTrack={this.trackRenderer}
|
||||
onChange={this.onSliderChange}
|
||||
onAfterChange={this.onAfterSliderChange}
|
||||
/>
|
||||
|
|
|
@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
|
|||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
|
|
@ -4,11 +4,13 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Tags from './Tags';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.tags,
|
||||
createSortedSectionSelector('tags', sortByProp('label')),
|
||||
(tags) => {
|
||||
const isFetching = tags.isFetching || tags.details.isFetching;
|
||||
const error = tags.error || tags.details.error;
|
||||
|
|
|
@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
|
|||
return function(getState, payload, dispatch) {
|
||||
const {
|
||||
id,
|
||||
...queryParams
|
||||
queryParams
|
||||
} = payload;
|
||||
|
||||
dispatch(set({ section, isDeleting: true }));
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
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 createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSetClientSideCollectionSortReducer
|
||||
from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
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 SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
||||
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
|
||||
|
@ -28,6 +36,9 @@ export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
|||
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
|
||||
export const saveCustomFormat = createThunk(SAVE_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) => {
|
||||
return {
|
||||
|
@ -47,20 +58,30 @@ export default {
|
|||
// State
|
||||
|
||||
defaultState: {
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: {
|
||||
includeCustomFormatWhenRenaming: false
|
||||
},
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: ({ name }) => {
|
||||
return name.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
|
@ -82,7 +103,10 @@ export default {
|
|||
}));
|
||||
|
||||
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;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
},
|
||||
|
||||
[SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -96,8 +96,8 @@ export default {
|
|||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
name: ({ name }) => {
|
||||
return name.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
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