mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-13 02:07:12 -07:00
Compare commits
No commits in common. "develop" and "v2.4.3.4248" have entirely different histories.
develop
...
v2.4.3.424
492 changed files with 6214 additions and 12286 deletions
|
@ -6,7 +6,7 @@
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"nodeGypDependencies": true,
|
"nodeGypDependencies": true,
|
||||||
"version": "20",
|
"version": "16",
|
||||||
"nvmVersion": "latest"
|
"nvmVersion": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -60,7 +60,6 @@ body:
|
||||||
- Master
|
- Master
|
||||||
- Develop
|
- Develop
|
||||||
- Nightly
|
- Nightly
|
||||||
- Plugins (experimental)
|
|
||||||
- Other (This issue will be closed)
|
- Other (This issue will be closed)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
|
@ -12,6 +12,6 @@ jobs:
|
||||||
action:
|
action:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/label-actions@v4
|
- uses: dessant/label-actions@v3
|
||||||
with:
|
with:
|
||||||
process-only: 'issues'
|
process-only: 'issues'
|
||||||
|
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5
|
- uses: dessant/lock-threads@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
|
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -121,7 +121,6 @@ _artifacts
|
||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
_temp*
|
|
||||||
*.Result.xml
|
*.Result.xml
|
||||||
coverage*.xml
|
coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
|
@ -140,6 +139,12 @@ project.fragment.lock.json
|
||||||
artifacts/
|
artifacts/
|
||||||
**/Properties/launchSettings.json
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
#VS outout folders
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
output/*
|
||||||
|
|
||||||
|
|
||||||
# macOS metadata files
|
# macOS metadata files
|
||||||
._*
|
._*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -158,12 +163,34 @@ Thumbs.db
|
||||||
/tools/Addins/*
|
/tools/Addins/*
|
||||||
packages.config.md5sum
|
packages.config.md5sum
|
||||||
|
|
||||||
|
|
||||||
|
# Common IntelliJ Platform excludes
|
||||||
|
|
||||||
|
# User specific
|
||||||
|
**/.idea/**/workspace.xml
|
||||||
|
**/.idea/**/tasks.xml
|
||||||
|
**/.idea/shelf/*
|
||||||
|
**/.idea/dictionaries
|
||||||
|
**/.idea/.idea.Radarr.Posix
|
||||||
|
**/.idea/.idea.Radarr.Windows
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
**/.idea/**/dataSources/
|
||||||
|
**/.idea/**/dataSources.ids
|
||||||
|
**/.idea/**/dataSources.xml
|
||||||
|
**/.idea/**/dataSources.local.xml
|
||||||
|
**/.idea/**/sqlDataSources.xml
|
||||||
|
**/.idea/**/dynamic.xml
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
# Rider auto-generates .iml files, and contentModel.xml
|
||||||
|
**/.idea/**/*.iml
|
||||||
|
**/.idea/**/contentModel.xml
|
||||||
|
**/.idea/**/modules.xml
|
||||||
|
|
||||||
# ignore node_modules symlink
|
# ignore node_modules symlink
|
||||||
node_modules
|
node_modules
|
||||||
node_modules.nosync
|
node_modules.nosync
|
||||||
|
|
||||||
# API doc generation
|
# API doc generation
|
||||||
.config/
|
.config/
|
||||||
|
|
||||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
|
||||||
.idea/
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Lidarr
|
# Lidarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
|
||||||
[](https://wiki.servarr.com/lidarr/installation#docker)
|
[](https://wiki.servarr.com/lidarr/installation#docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
|
@ -9,9 +8,6 @@
|
||||||
|
|
||||||
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> NOTICE - The Lidarr Metadata Server is currently down impacting adding artists, etc. Please follow [GHI 5498](https://github.com/Lidarr/Lidarr/issues/5498) or see Discord for detaila.
|
|
||||||
|
|
||||||
## Major Features Include:
|
## Major Features Include:
|
||||||
|
|
||||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||||
|
|
|
@ -9,18 +9,18 @@ variables:
|
||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '2.13.2'
|
majorVersion: '2.4.3'
|
||||||
minorVersion: $[counter('minorVersion', 1076)]
|
minorVersion: $[counter('minorVersion', 1076)]
|
||||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.427'
|
dotnetVersion: '6.0.421'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-22.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-12'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
|
@ -1120,19 +1120,19 @@ stages:
|
||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@2
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'cli'
|
scannerMode: 'CLI'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'lidarr_Lidarr.UI'
|
cliProjectKey: 'lidarr_Lidarr.UI'
|
||||||
cliProjectName: 'LidarrUI'
|
cliProjectName: 'LidarrUI'
|
||||||
cliProjectVersion: '$(lidarrVersion)'
|
cliProjectVersion: '$(lidarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@2
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
|
@ -1208,12 +1208,12 @@ stages:
|
||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'dotnet'
|
scannerMode: 'MSBuild'
|
||||||
projectKey: 'lidarr_Lidarr'
|
projectKey: 'lidarr_Lidarr'
|
||||||
projectName: 'Lidarr'
|
projectName: 'Lidarr'
|
||||||
projectVersion: '$(lidarrVersion)'
|
projectVersion: '$(lidarrVersion)'
|
||||||
|
@ -1226,10 +1226,10 @@ stages:
|
||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5.3.11
|
- task: reportgenerator@5
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
|
|
|
@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||||
app_guid=${app_guid:-media}
|
app_guid=${app_guid:-media}
|
||||||
|
|
||||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||||
|
|
||||||
# Create User / Group as needed
|
# Create User / Group as needed
|
||||||
|
@ -114,7 +114,7 @@ case "$ARCH" in
|
||||||
esac
|
esac
|
||||||
echo ""
|
echo ""
|
||||||
echo "Removing previous tarballs"
|
echo "Removing previous tarballs"
|
||||||
# -f to Force so we fail if it doesn't exist
|
# -f to Force so we fail if it doesnt exist
|
||||||
rm -f "${app^}".*.tar.gz
|
rm -f "${app^}".*.tar.gz
|
||||||
echo ""
|
echo ""
|
||||||
echo "Downloading..."
|
echo "Downloading..."
|
||||||
|
|
15
docs.sh
15
docs.sh
|
@ -1,18 +1,13 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
FRAMEWORK="net6.0"
|
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
ARCHITECTURE="${2:-x64}"
|
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-$ARCHITECTURE"
|
RUNTIME="win-x64"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-$ARCHITECTURE"
|
RUNTIME="linux-x64"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-$ARCHITECTURE"
|
RUNTIME="osx-x64"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -40,7 +35,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v1 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (env) => {
|
||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
target: 'web',
|
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
|
@ -68,7 +67,7 @@ module.exports = (env) => {
|
||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
filename: '[name]-[contenthash].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -93,7 +92,7 @@ module.exports = (env) => {
|
||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
|
@ -135,12 +134,6 @@ module.exports = (env) => {
|
||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, 'Content/robots.txt')
|
destination: path.join(distFolder, 'Content/robots.txt')
|
||||||
},
|
|
||||||
|
|
||||||
// manifest.json and browserconfig.xml
|
|
||||||
{
|
|
||||||
source: 'frontend/src/Content/*.(json|xml)',
|
|
||||||
destination: path.join(distFolder, 'Content')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -188,7 +181,7 @@ module.exports = (env) => {
|
||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: '3.41'
|
corejs: 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -209,7 +202,7 @@ module.exports = (env) => {
|
||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,6 @@ const mixinsFiles = [
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
'autoprefixer',
|
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -172,8 +172,7 @@ function HistoryDetails(props) {
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message,
|
message
|
||||||
indexer
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -193,14 +192,6 @@ function HistoryDetails(props) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
indexer ? (
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
message ?
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|
|
@ -26,5 +26,4 @@
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 90px;
|
width: 90px;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,40 +57,30 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
iconName = icons.PAUSED;
|
iconName = icons.PAUSED;
|
||||||
title = translate('Paused');
|
title = 'Paused';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'queued') {
|
if (status === 'queued') {
|
||||||
iconName = icons.QUEUED;
|
iconName = icons.QUEUED;
|
||||||
title = translate('Queued');
|
title = 'Queued';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = translate('Downloaded');
|
title = 'Downloaded';
|
||||||
|
|
||||||
if (trackedDownloadState === 'importBlocked') {
|
|
||||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importFailed') {
|
|
||||||
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
if (trackedDownloadState === 'importPending') {
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
title += ' - Waiting to Import';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
if (trackedDownloadState === 'importing') {
|
||||||
title += ` - ${translate('Importing')}`;
|
title += ' - Importing';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
if (trackedDownloadState === 'failedPending') {
|
||||||
title += ` - ${translate('WaitingToProcess')}`;
|
title += ' - Waiting to Process';
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,38 +91,36 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
title = translate('Pending');
|
title = 'Pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = translate('PendingDownloadClientUnavailable');
|
title = 'Pending - Download client is unavailable';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'failed') {
|
if (status === 'failed') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage =
|
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||||
errorMessage || translate('CheckDownloadClientForDetails');
|
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOAD;
|
iconName = icons.DOWNLOAD;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('ImportFailed', { sourceTitle });
|
title = `Import failed: ${sourceTitle}`;
|
||||||
} else {
|
} else {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,6 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
{
|
{
|
||||||
key: 'blocklistAndSearch',
|
key: 'blocklistAndSearch',
|
||||||
value: translate('BlocklistAndSearch'),
|
value: translate('BlocklistAndSearch'),
|
||||||
isDisabled: isPending,
|
|
||||||
hint: multipleSelected
|
hint: multipleSelected
|
||||||
? translate('BlocklistAndSearchMultipleHint')
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
: translate('BlocklistAndSearchHint'),
|
: translate('BlocklistAndSearchHint'),
|
||||||
|
@ -131,7 +130,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
: translate('BlocklistOnlyHint'),
|
: translate('BlocklistOnlyHint'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [isPending, multipleSelected]);
|
}, [multipleSelected]);
|
||||||
|
|
||||||
const handleRemovalMethodChange = useCallback(
|
const handleRemovalMethodChange = useCallback(
|
||||||
({ value }: { value: RemovalMethod }) => {
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
|
|
@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('AllAlbums')}
|
title={translate('AllAlbums')}
|
||||||
data={translate('MonitorAllAlbums')}
|
data="Monitor all new albums"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('NewAlbums')}
|
title={translate('NewAlbums')}
|
||||||
data={translate('MonitorNewAlbumsData')}
|
data="Monitor new albums released after the newest existing album"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('None')}
|
title={translate('None')}
|
||||||
data={translate('MonitorNoAlbumsData')}
|
data="Don't monitor any new albums"
|
||||||
/>
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,6 @@ export interface Statistics {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Album extends ModelBase {
|
interface Album extends ModelBase {
|
||||||
artistId: number;
|
|
||||||
artist: Artist;
|
artist: Artist;
|
||||||
foreignAlbumId: string;
|
foreignAlbumId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -20,7 +19,6 @@ interface Album extends ModelBase {
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
lastSearchTime?: string;
|
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,10 @@ import Link from 'Components/Link/Link';
|
||||||
|
|
||||||
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
||||||
const link = `/album/${foreignAlbumId}`;
|
const link = `/album/${foreignAlbumId}`;
|
||||||
const albumTitle = `${title}${disambiguation ? ` (${disambiguation})` : ''}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={link} title={albumTitle}>
|
<Link to={link}>
|
||||||
{albumTitle}
|
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,8 +121,6 @@
|
||||||
|
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.sizeOnDisk,
|
.sizeOnDisk,
|
||||||
.albumType,
|
|
||||||
.secondaryTypes,
|
|
||||||
.qualityProfileName,
|
.qualityProfileName,
|
||||||
.links,
|
.links,
|
||||||
.tags {
|
.tags {
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'albumNavigationButton': string;
|
'albumNavigationButton': string;
|
||||||
'albumNavigationButtons': string;
|
'albumNavigationButtons': string;
|
||||||
'albumType': string;
|
|
||||||
'alternateTitlesIconContainer': string;
|
'alternateTitlesIconContainer': string;
|
||||||
'backdrop': string;
|
'backdrop': string;
|
||||||
'backdropOverlay': string;
|
'backdropOverlay': string;
|
||||||
|
@ -21,7 +20,6 @@ interface CssExports {
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'qualityProfileName': string;
|
'qualityProfileName': string;
|
||||||
'releaseDate': string;
|
'releaseDate': string;
|
||||||
'secondaryTypes': string;
|
|
||||||
'sizeOnDisk': string;
|
'sizeOnDisk': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
|
|
@ -192,7 +192,6 @@ class AlbumDetails extends Component {
|
||||||
duration,
|
duration,
|
||||||
overview,
|
overview,
|
||||||
albumType,
|
albumType,
|
||||||
secondaryTypes,
|
|
||||||
statistics = {},
|
statistics = {},
|
||||||
monitored,
|
monitored,
|
||||||
releaseDate,
|
releaseDate,
|
||||||
|
@ -205,7 +204,6 @@ class AlbumDetails extends Component {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
tracksError,
|
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
hasTrackFiles,
|
hasTrackFiles,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
|
@ -398,11 +396,10 @@ class AlbumDetails extends Component {
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
duration ?
|
!!duration &&
|
||||||
<span className={styles.duration}>
|
<span className={styles.duration}>
|
||||||
{formatDuration(duration)}
|
{formatDuration(duration)}
|
||||||
</span> :
|
</span>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<HeartRating
|
<HeartRating
|
||||||
|
@ -421,15 +418,14 @@ class AlbumDetails extends Component {
|
||||||
title={translate('ReleaseDate')}
|
title={translate('ReleaseDate')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.CALENDAR}
|
name={icons.CALENDAR}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.releaseDate}>
|
<span className={styles.releaseDate}>
|
||||||
{moment(releaseDate).format(shortDateFormat)}
|
{moment(releaseDate).format(shortDateFormat)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -438,15 +434,16 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.DRIVE}
|
name={icons.DRIVE}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.sizeOnDisk}>
|
<span className={styles.sizeOnDisk}>
|
||||||
{formatBytes(sizeOnDisk)}
|
{
|
||||||
|
formatBytes(sizeOnDisk || 0)
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
|
@ -462,55 +459,32 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{
|
{
|
||||||
albumType ?
|
!!albumType &&
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
title={translate('Type')}
|
title={translate('Type')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.INFO}
|
name={icons.INFO}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
<span className={styles.albumType}>
|
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
{albumType}
|
{albumType}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Label>
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
secondaryTypes.length ?
|
|
||||||
<Label
|
|
||||||
className={styles.detailsLabel}
|
|
||||||
title={translate('SecondaryTypes')}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
name={icons.INFO}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
<span className={styles.secondaryTypes}>
|
|
||||||
{secondaryTypes.join(', ')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -519,15 +493,14 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.EXTERNAL_LINK}
|
name={icons.EXTERNAL_LINK}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
{translate('Links')}
|
{translate('Links')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
|
@ -553,9 +526,8 @@ class AlbumDetails extends Component {
|
||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
!isPopulated && !albumsError && !trackFilesError &&
|
||||||
<LoadingIndicator /> :
|
<LoadingIndicator />
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -566,14 +538,6 @@ class AlbumDetails extends Component {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && tracksError ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('TracksLoadError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && trackFilesError ?
|
!isFetching && trackFilesError ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
|
@ -602,14 +566,6 @@ class AlbumDetails extends Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !media.length ?
|
|
||||||
<Alert kind={kinds.WARNING}>
|
|
||||||
{translate('NoMediumInformation')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrganizePreviewModalConnector
|
<OrganizePreviewModalConnector
|
||||||
|
@ -676,7 +632,6 @@ AlbumDetails.propTypes = {
|
||||||
duration: PropTypes.number,
|
duration: PropTypes.number,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
albumType: PropTypes.string.isRequired,
|
albumType: PropTypes.string.isRequired,
|
||||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
|
@ -703,8 +658,6 @@ AlbumDetails.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
AlbumDetails.defaultProps = {
|
AlbumDetails.defaultProps = {
|
||||||
secondaryTypes: [],
|
|
||||||
statistics: {},
|
|
||||||
isSaving: false
|
isSaving: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,16 @@ import TrackRowConnector from './TrackRowConnector';
|
||||||
import styles from './AlbumDetailsMedium.css';
|
import styles from './AlbumDetailsMedium.css';
|
||||||
|
|
||||||
function getMediumStatistics(tracks) {
|
function getMediumStatistics(tracks) {
|
||||||
const trackCount = tracks.length;
|
let trackCount = 0;
|
||||||
let trackFileCount = 0;
|
let trackFileCount = 0;
|
||||||
let totalTrackCount = 0;
|
let totalTrackCount = 0;
|
||||||
|
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
if (track.trackFileId) {
|
if (track.trackFileId) {
|
||||||
|
trackCount++;
|
||||||
trackFileCount++;
|
trackFileCount++;
|
||||||
|
} else {
|
||||||
|
trackCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTrackCount++;
|
totalTrackCount++;
|
||||||
|
|
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import Updates from 'System/Updates/Updates';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={Updates}
|
component={UpdatesConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -44,7 +44,6 @@ export interface CustomFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
version: string;
|
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import AppSectionState, {
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
|
@ -13,16 +12,13 @@ import MetadataProfile from 'typings/MetadataProfile';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import RootFolder from 'typings/RootFolder';
|
import RootFolder from 'typings/RootFolder';
|
||||||
import General from 'typings/Settings/General';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export type GeneralAppState = AppSectionItemState<General>;
|
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
@ -45,11 +41,6 @@ export interface MetadataProfilesAppState
|
||||||
extends AppSectionState<MetadataProfile>,
|
extends AppSectionState<MetadataProfile>,
|
||||||
AppSectionSchemaState<MetadataProfile> {}
|
AppSectionSchemaState<MetadataProfile> {}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
|
||||||
extends AppSectionState<CustomFormat>,
|
|
||||||
AppSectionDeleteState,
|
|
||||||
AppSectionSaveState {}
|
|
||||||
|
|
||||||
export interface RootFolderAppState
|
export interface RootFolderAppState
|
||||||
extends AppSectionState<RootFolder>,
|
extends AppSectionState<RootFolder>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
@ -59,10 +50,7 @@ export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
|
||||||
customFormats: CustomFormatAppState;
|
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
general: GeneralAppState;
|
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import Update from 'typings/Update';
|
import { AppSectionItemState } from './AppSectionState';
|
||||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
|
||||||
|
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
export type UpdateAppState = AppSectionState<Update>;
|
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
updates: UpdateAppState;
|
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface Ratings {
|
||||||
|
|
||||||
interface Artist extends ModelBase {
|
interface Artist extends ModelBase {
|
||||||
added: string;
|
added: string;
|
||||||
|
artistMetadataId: string;
|
||||||
foreignArtistId: string;
|
foreignArtistId: string;
|
||||||
cleanName: string;
|
cleanName: string;
|
||||||
ended: boolean;
|
ended: boolean;
|
||||||
|
|
|
@ -10,7 +10,6 @@ function AlbumGroupInfo(props) {
|
||||||
const {
|
const {
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
albumFileCount,
|
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
sizeOnDisk
|
sizeOnDisk
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -31,13 +30,6 @@ function AlbumGroupInfo(props) {
|
||||||
data={monitoredAlbumCount}
|
data={monitoredAlbumCount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
titleClassName={styles.title}
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('WithFiles')}
|
|
||||||
data={albumFileCount}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
titleClassName={styles.title}
|
titleClassName={styles.title}
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
|
@ -58,7 +50,6 @@ function AlbumGroupInfo(props) {
|
||||||
AlbumGroupInfo.propTypes = {
|
AlbumGroupInfo.propTypes = {
|
||||||
totalAlbumCount: PropTypes.number.isRequired,
|
totalAlbumCount: PropTypes.number.isRequired,
|
||||||
monitoredAlbumCount: PropTypes.number.isRequired,
|
monitoredAlbumCount: PropTypes.number.isRequired,
|
||||||
albumFileCount: PropTypes.number.isRequired,
|
|
||||||
trackFileCount: PropTypes.number.isRequired,
|
trackFileCount: PropTypes.number.isRequired,
|
||||||
sizeOnDisk: PropTypes.number.isRequired
|
sizeOnDisk: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -149,7 +149,9 @@ class AlbumRow extends Component {
|
||||||
if (name === 'secondaryTypes') {
|
if (name === 'secondaryTypes') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{secondaryTypes.join(', ')}
|
{
|
||||||
|
secondaryTypes
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -158,7 +160,7 @@ class AlbumRow extends Component {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{
|
{
|
||||||
totalTrackCount
|
statistics.totalTrackCount
|
||||||
}
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,43 +22,32 @@ import styles from './ArtistDetailsSeason.css';
|
||||||
|
|
||||||
function getAlbumStatistics(albums) {
|
function getAlbumStatistics(albums) {
|
||||||
let albumCount = 0;
|
let albumCount = 0;
|
||||||
let albumFileCount = 0;
|
|
||||||
let trackFileCount = 0;
|
let trackFileCount = 0;
|
||||||
let totalAlbumCount = 0;
|
let totalAlbumCount = 0;
|
||||||
let monitoredAlbumCount = 0;
|
let monitoredAlbumCount = 0;
|
||||||
let hasMonitoredAlbums = false;
|
let hasMonitoredAlbums = false;
|
||||||
let sizeOnDisk = 0;
|
let sizeOnDisk = 0;
|
||||||
|
|
||||||
albums.forEach(({ monitored, releaseDate, statistics = {} }) => {
|
albums.forEach((album) => {
|
||||||
const {
|
if (album.statistics) {
|
||||||
trackFileCount: albumTrackFileCount = 0,
|
sizeOnDisk = sizeOnDisk + album.statistics.sizeOnDisk;
|
||||||
totalTrackCount: albumTotalTrackCount = 0,
|
trackFileCount = trackFileCount + album.statistics.trackFileCount;
|
||||||
sizeOnDisk: albumSizeOnDisk = 0
|
|
||||||
} = statistics;
|
|
||||||
|
|
||||||
const hasFiles = albumTrackFileCount > 0 && albumTrackFileCount === albumTotalTrackCount;
|
if (album.statistics.trackFileCount === album.statistics.totalTrackCount || (album.monitored && isBefore(album.airDateUtc))) {
|
||||||
|
|
||||||
if (hasFiles || (monitored && isBefore(releaseDate))) {
|
|
||||||
albumCount++;
|
albumCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFiles) {
|
|
||||||
albumFileCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monitored) {
|
if (album.monitored) {
|
||||||
monitoredAlbumCount++;
|
monitoredAlbumCount++;
|
||||||
hasMonitoredAlbums = true;
|
hasMonitoredAlbums = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalAlbumCount++;
|
totalAlbumCount++;
|
||||||
trackFileCount = trackFileCount + albumTrackFileCount;
|
|
||||||
sizeOnDisk = sizeOnDisk + albumSizeOnDisk;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount,
|
albumCount,
|
||||||
albumFileCount,
|
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
|
@ -67,8 +56,8 @@ function getAlbumStatistics(albums) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAlbumCountKind(monitored, albumCount, albumFileCount) {
|
function getAlbumCountKind(monitored, albumCount, monitoredAlbumCount) {
|
||||||
if (albumCount === albumFileCount && albumFileCount > 0) {
|
if (albumCount === monitoredAlbumCount && monitoredAlbumCount > 0) {
|
||||||
return kinds.SUCCESS;
|
return kinds.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +192,6 @@ class ArtistDetailsSeason extends Component {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
albumCount,
|
albumCount,
|
||||||
albumFileCount,
|
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
|
@ -238,9 +226,9 @@ class ArtistDetailsSeason extends Component {
|
||||||
anchor={
|
anchor={
|
||||||
<Label
|
<Label
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, albumFileCount)}
|
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, monitoredAlbumCount)}
|
||||||
>
|
>
|
||||||
<span>{albumFileCount} / {albumCount}</span>
|
<span>{albumCount} / {monitoredAlbumCount}</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
title={translate('GroupInformation')}
|
title={translate('GroupInformation')}
|
||||||
|
@ -249,7 +237,6 @@ class ArtistDetailsSeason extends Component {
|
||||||
<AlbumGroupInfo
|
<AlbumGroupInfo
|
||||||
totalAlbumCount={totalAlbumCount}
|
totalAlbumCount={totalAlbumCount}
|
||||||
monitoredAlbumCount={monitoredAlbumCount}
|
monitoredAlbumCount={monitoredAlbumCount}
|
||||||
albumFileCount={albumFileCount}
|
|
||||||
trackFileCount={trackFileCount}
|
trackFileCount={trackFileCount}
|
||||||
sizeOnDisk={sizeOnDisk}
|
sizeOnDisk={sizeOnDisk}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import ArtistTags from './ArtistTags';
|
import ArtistTags from './ArtistTags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -13,8 +12,8 @@ function createMapStateToProps() {
|
||||||
const tags = artist.tags
|
const tags = artist.tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((tag) => !!tag)
|
.filter((tag) => !!tag)
|
||||||
.sort(sortByProp('label'))
|
.map((tag) => tag.label)
|
||||||
.map((tag) => tag.label);
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags
|
tags
|
||||||
|
|
|
@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditArtistModalContent.css';
|
import styles from './EditArtistModalContent.css';
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form {...otherProps}>
|
<Form {...otherProps}>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Monitored')}
|
{translate('Monitored')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -107,10 +107,9 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MonitorNewItems')}
|
{translate('MonitorNewItems')}
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -133,7 +132,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('QualityProfile')}
|
{translate('QualityProfile')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -147,10 +146,10 @@ class EditArtistModalContent extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
showMetadataProfile ?
|
showMetadataProfile &&
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MetadataProfile')}
|
Metadata Profile
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
|
@ -174,11 +173,10 @@ class EditArtistModalContent extends Component {
|
||||||
{...metadataProfileId}
|
{...metadataProfileId}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup> :
|
</FormGroup>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Path')}
|
{translate('Path')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -191,7 +189,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Tags')}
|
{translate('Tags')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -211,7 +209,7 @@ class EditArtistModalContent extends Component {
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={onDeleteArtistPress}
|
onPress={onDeleteArtistPress}
|
||||||
>
|
>
|
||||||
{translate('Delete')}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -79,7 +79,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('LastAlbum')}
|
{translate('Last Album')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
helpText={translate('ICalTagsArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -3,8 +3,8 @@ import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import PathInput from 'Components/Form/PathInput';
|
import PathInput from 'Components/Form/PathInput';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
|
||||||
className={styles.mappedDrivesWarning}
|
className={styles.mappedDrivesWarning}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/lidarr/faq#why-cant-lidarr-see-my-files-on-a-remote-server' })} />
|
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/lidarr/faq">FAQ</Link> for more information.
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ function ArtistFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
|
||||||
const tagList = allArtists
|
const tagList = allArtists
|
||||||
.map((artist) => ({ id: artist.id, name: artist.artistName }))
|
.map((artist) => ({ id: artist.id, name: artist.artistName }))
|
||||||
.sort(sortByProp('name'));
|
.sort(sortByName);
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React, { Component } from 'react';
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue';
|
import ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue';
|
||||||
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
|
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
|
||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
|
@ -11,11 +10,11 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue';
|
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
|
||||||
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||||
import styles from './FilterBuilderRow.css';
|
import styles from './FilterBuilderRow.css';
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
return IndexerFilterBuilderRowValueConnector;
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.METADATA_PROFILE:
|
case filterBuilderValueTypes.METADATA_PROFILE:
|
||||||
return MetadataProfileFilterBuilderRowValue;
|
return MetadataProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
||||||
return MonitorNewItemsFilterBuilderRowValue;
|
return MonitorNewItemsFilterBuilderRowValue;
|
||||||
|
@ -80,7 +79,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
return QualityFilterBuilderRowValueConnector;
|
return QualityFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValue;
|
return QualityProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
case filterBuilderValueTypes.ARTIST:
|
case filterBuilderValueTypes.ARTIST:
|
||||||
return ArtistFilterBuilderRowValue;
|
return ArtistFilterBuilderRowValue;
|
||||||
|
@ -225,7 +224,7 @@ class FilterBuilderRow extends Component {
|
||||||
key: name,
|
key: name,
|
||||||
value: typeof label === 'function' ? label() : label
|
value: typeof label === 'function' ? label() : label
|
||||||
};
|
};
|
||||||
}).sort(sortByProp('value'));
|
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||||
|
|
||||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { filterBuilderTypes } from 'Helpers/Props';
|
import { filterBuilderTypes } from 'Helpers/Props';
|
||||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
function createTagListSelector() {
|
function createTagListSelector() {
|
||||||
|
@ -38,7 +38,7 @@ function createTagListSelector() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []).sort(sortByProp('name'));
|
}, []).sort(sortByName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.uniqBy(items, 'id');
|
return _.uniqBy(items, 'id');
|
||||||
|
|
|
@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
get name() {
|
get name() {
|
||||||
return translate('ImportCompleteFailed');
|
return translate('ImportFailed');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
|
||||||
|
|
||||||
function createMetadataProfilesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.settings.metadataProfiles.items,
|
|
||||||
(metadataProfiles) => {
|
|
||||||
return metadataProfiles;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MetadataProfileFilterBuilderRowValue(
|
|
||||||
props: FilterBuilderRowValueProps
|
|
||||||
) {
|
|
||||||
const metadataProfiles = useSelector(createMetadataProfilesSelector());
|
|
||||||
|
|
||||||
const tagList = metadataProfiles
|
|
||||||
.map(({ id, name }) => ({ id, name }))
|
|
||||||
.sort(sortByProp('name'));
|
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MetadataProfileFilterBuilderRowValue;
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.metadataProfiles,
|
||||||
|
(metadataProfiles) => {
|
||||||
|
const tagList = metadataProfiles.items.map((metadataProfile) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
} = metadataProfile;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -1,30 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
|
||||||
|
|
||||||
function createQualityProfilesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.settings.qualityProfiles.items,
|
|
||||||
(qualityProfiles) => {
|
|
||||||
return qualityProfiles;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function QualityProfileFilterBuilderRowValue(
|
|
||||||
props: FilterBuilderRowValueProps
|
|
||||||
) {
|
|
||||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
|
||||||
|
|
||||||
const tagList = qualityProfiles
|
|
||||||
.map(({ id, name }) => ({ id, name }))
|
|
||||||
.sort(sortByProp('name'));
|
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QualityProfileFilterBuilderRowValue;
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.qualityProfiles,
|
||||||
|
(qualityProfiles) => {
|
||||||
|
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
} = qualityProfile;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -5,7 +5,6 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CustomFilter from './CustomFilter';
|
import CustomFilter from './CustomFilter';
|
||||||
import styles from './CustomFiltersModalContent.css';
|
import styles from './CustomFiltersModalContent.css';
|
||||||
|
@ -32,7 +31,7 @@ function CustomFiltersModalContent(props) {
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
.map((customFilter) => {
|
.map((customFilter) => {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
|
|
|
@ -4,8 +4,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -23,7 +22,7 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
||||||
|
|
||||||
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||||
return {
|
return {
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name,
|
value: downloadClient.name,
|
||||||
|
@ -34,7 +33,7 @@ function createMapStateToProps() {
|
||||||
if (includeAny) {
|
if (includeAny) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 0,
|
key: 0,
|
||||||
value: `(${translate('Any')})`
|
value: '(Any)'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
}
|
}
|
||||||
|
@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
bottom
|
||||||
|
} = data.offsets.reference;
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
if ((/^botton/).test(data.placement)) {
|
||||||
|
data.styles.maxHeight = windowHeight - bottom;
|
||||||
|
} else {
|
||||||
|
data.styles.maxHeight = top;
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component {
|
||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: true,
|
|
||||||
boundariesElement: 'viewport'
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -49,12 +49,12 @@ function getComponent(type) {
|
||||||
case inputTypes.DEVICE:
|
case inputTypes.DEVICE:
|
||||||
return DeviceInputConnector;
|
return DeviceInputConnector;
|
||||||
|
|
||||||
case inputTypes.KEY_VALUE_LIST:
|
|
||||||
return KeyValueListInput;
|
|
||||||
|
|
||||||
case inputTypes.PLAYLIST:
|
case inputTypes.PLAYLIST:
|
||||||
return PlaylistInputConnector;
|
return PlaylistInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.KEY_VALUE_LIST:
|
||||||
|
return KeyValueListInput;
|
||||||
|
|
||||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||||
return MonitorAlbumsSelectInput;
|
return MonitorAlbumsSelectInput;
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -20,7 +19,7 @@ function createMapStateToProps() {
|
||||||
items
|
items
|
||||||
} = indexers;
|
} = indexers;
|
||||||
|
|
||||||
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
|
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||||
return {
|
return {
|
||||||
key: indexer.id,
|
key: indexer.id,
|
||||||
value: indexer.name
|
value: indexer.name
|
||||||
|
@ -30,7 +29,7 @@ function createMapStateToProps() {
|
||||||
if (includeAny) {
|
if (includeAny) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 0,
|
key: 0,
|
||||||
value: `(${translate('Any')})`
|
value: '(Any)'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
156
frontend/src/Components/Form/KeyValueListInput.js
Normal file
156
frontend/src/Components/Form/KeyValueListInput.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import KeyValueListInputItem from './KeyValueListInputItem';
|
||||||
|
import styles from './KeyValueListInput.css';
|
||||||
|
|
||||||
|
class KeyValueListInput extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isFocused: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onItemChange = (index, itemValue) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = [...value];
|
||||||
|
|
||||||
|
if (index == null) {
|
||||||
|
newValue.push(itemValue);
|
||||||
|
} else {
|
||||||
|
newValue.splice(index, 1, itemValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveItem = (index) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
this.setState({
|
||||||
|
isFocused: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.setState({
|
||||||
|
isFocused: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newValue = value.reduce((acc, v) => {
|
||||||
|
if (v.key || v.value) {
|
||||||
|
acc.push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (newValue.length !== value.length) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
keyPlaceholder,
|
||||||
|
valuePlaceholder,
|
||||||
|
hasError,
|
||||||
|
hasWarning
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { isFocused } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(
|
||||||
|
className,
|
||||||
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
[...value, { key: '', value: '' }].map((v, index) => {
|
||||||
|
return (
|
||||||
|
<KeyValueListInputItem
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
keyValue={v.key}
|
||||||
|
value={v.value}
|
||||||
|
keyPlaceholder={keyPlaceholder}
|
||||||
|
valuePlaceholder={valuePlaceholder}
|
||||||
|
isNew={index === value.length}
|
||||||
|
onChange={this.onItemChange}
|
||||||
|
onRemove={this.onRemoveItem}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValueListInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
keyPlaceholder: PropTypes.string,
|
||||||
|
valuePlaceholder: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyValueListInput.defaultProps = {
|
||||||
|
className: styles.inputContainer,
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyValueListInput;
|
|
@ -1,104 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { InputOnChange } from 'typings/inputs';
|
|
||||||
import KeyValueListInputItem from './KeyValueListInputItem';
|
|
||||||
import styles from './KeyValueListInput.css';
|
|
||||||
|
|
||||||
interface KeyValue {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KeyValueListInputProps {
|
|
||||||
className?: string;
|
|
||||||
name: string;
|
|
||||||
value: KeyValue[];
|
|
||||||
hasError?: boolean;
|
|
||||||
hasWarning?: boolean;
|
|
||||||
keyPlaceholder?: string;
|
|
||||||
valuePlaceholder?: string;
|
|
||||||
onChange: InputOnChange<KeyValue[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function KeyValueListInput({
|
|
||||||
className = styles.inputContainer,
|
|
||||||
name,
|
|
||||||
value = [],
|
|
||||||
hasError = false,
|
|
||||||
hasWarning = false,
|
|
||||||
keyPlaceholder,
|
|
||||||
valuePlaceholder,
|
|
||||||
onChange,
|
|
||||||
}: KeyValueListInputProps): JSX.Element {
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
|
|
||||||
const handleItemChange = useCallback(
|
|
||||||
(index: number | null, itemValue: KeyValue) => {
|
|
||||||
const newValue = [...value];
|
|
||||||
|
|
||||||
if (index === null) {
|
|
||||||
newValue.push(itemValue);
|
|
||||||
} else {
|
|
||||||
newValue.splice(index, 1, itemValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({ name, value: newValue });
|
|
||||||
},
|
|
||||||
[value, name, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemoveItem = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const newValue = [...value];
|
|
||||||
newValue.splice(index, 1);
|
|
||||||
onChange({ name, value: newValue });
|
|
||||||
},
|
|
||||||
[value, name, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFocus = useCallback(() => setIsFocused(true), []);
|
|
||||||
|
|
||||||
const onBlur = useCallback(() => {
|
|
||||||
setIsFocused(false);
|
|
||||||
|
|
||||||
const newValue = value.reduce((acc: KeyValue[], v) => {
|
|
||||||
if (v.key || v.value) {
|
|
||||||
acc.push(v);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (newValue.length !== value.length) {
|
|
||||||
onChange({ name, value: newValue });
|
|
||||||
}
|
|
||||||
}, [value, name, onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isFocused && styles.isFocused,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{[...value, { key: '', value: '' }].map((v, index) => (
|
|
||||||
<KeyValueListInputItem
|
|
||||||
key={index}
|
|
||||||
index={index}
|
|
||||||
keyValue={v.key}
|
|
||||||
value={v.value}
|
|
||||||
keyPlaceholder={keyPlaceholder}
|
|
||||||
valuePlaceholder={valuePlaceholder}
|
|
||||||
isNew={index === value.length}
|
|
||||||
onChange={handleItemChange}
|
|
||||||
onRemove={handleRemoveItem}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyValueListInput;
|
|
|
@ -5,19 +5,13 @@
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
border-bottom: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyInputWrapper {
|
.inputWrapper {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.valueInputWrapper {
|
|
||||||
flex: 1 0 0;
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWrapper {
|
.buttonWrapper {
|
||||||
flex: 0 0 22px;
|
flex: 0 0 22px;
|
||||||
}
|
}
|
||||||
|
@ -26,10 +20,6 @@
|
||||||
.valueInput {
|
.valueInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: var(--inputBackgroundColor);
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--helpTextColor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'buttonWrapper': string;
|
'buttonWrapper': string;
|
||||||
|
'inputWrapper': string;
|
||||||
'itemContainer': string;
|
'itemContainer': string;
|
||||||
'keyInput': string;
|
'keyInput': string;
|
||||||
'keyInputWrapper': string;
|
|
||||||
'valueInput': string;
|
'valueInput': string;
|
||||||
'valueInputWrapper': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
124
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
124
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './KeyValueListInputItem.css';
|
||||||
|
|
||||||
|
class KeyValueListInputItem extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onKeyChange = ({ value: keyValue }) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onValueChange = ({ value }) => {
|
||||||
|
// TODO: Validate here or validate at a lower level component
|
||||||
|
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
keyValue,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemovePress = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
onRemove
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onRemove(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
this.props.onFocus();
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.props.onBlur();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
keyValue,
|
||||||
|
value,
|
||||||
|
keyPlaceholder,
|
||||||
|
valuePlaceholder,
|
||||||
|
isNew
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemContainer}>
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.keyInput}
|
||||||
|
name="key"
|
||||||
|
value={keyValue}
|
||||||
|
placeholder={keyPlaceholder}
|
||||||
|
onChange={this.onKeyChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.valueInput}
|
||||||
|
name="value"
|
||||||
|
value={value}
|
||||||
|
placeholder={valuePlaceholder}
|
||||||
|
onChange={this.onValueChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonWrapper}>
|
||||||
|
{
|
||||||
|
isNew ?
|
||||||
|
null :
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
tabIndex={-1}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValueListInputItem.propTypes = {
|
||||||
|
index: PropTypes.number,
|
||||||
|
keyValue: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
keyPlaceholder: PropTypes.string.isRequired,
|
||||||
|
valuePlaceholder: PropTypes.string.isRequired,
|
||||||
|
isNew: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
onFocus: PropTypes.func.isRequired,
|
||||||
|
onBlur: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyValueListInputItem.defaultProps = {
|
||||||
|
keyPlaceholder: 'Key',
|
||||||
|
valuePlaceholder: 'Value'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyValueListInputItem;
|
|
@ -1,89 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './KeyValueListInputItem.css';
|
|
||||||
|
|
||||||
interface KeyValueListInputItemProps {
|
|
||||||
index: number;
|
|
||||||
keyValue: string;
|
|
||||||
value: string;
|
|
||||||
keyPlaceholder?: string;
|
|
||||||
valuePlaceholder?: string;
|
|
||||||
isNew: boolean;
|
|
||||||
onChange: (index: number, itemValue: { key: string; value: string }) => void;
|
|
||||||
onRemove: (index: number) => void;
|
|
||||||
onFocus: () => void;
|
|
||||||
onBlur: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function KeyValueListInputItem({
|
|
||||||
index,
|
|
||||||
keyValue,
|
|
||||||
value,
|
|
||||||
keyPlaceholder = 'Key',
|
|
||||||
valuePlaceholder = 'Value',
|
|
||||||
isNew,
|
|
||||||
onChange,
|
|
||||||
onRemove,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
}: KeyValueListInputItemProps): JSX.Element {
|
|
||||||
const handleKeyChange = useCallback(
|
|
||||||
({ value: keyValue }: { value: string }) => {
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
},
|
|
||||||
[index, value, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleValueChange = useCallback(
|
|
||||||
({ value }: { value: string }) => {
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
},
|
|
||||||
[index, keyValue, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemovePress = useCallback(() => {
|
|
||||||
onRemove(index);
|
|
||||||
}, [index, onRemove]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.itemContainer}>
|
|
||||||
<div className={styles.keyInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.keyInput}
|
|
||||||
name="key"
|
|
||||||
value={keyValue}
|
|
||||||
placeholder={keyPlaceholder}
|
|
||||||
onChange={handleKeyChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.valueInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.valueInput}
|
|
||||||
name="value"
|
|
||||||
value={value}
|
|
||||||
placeholder={valuePlaceholder}
|
|
||||||
onChange={handleValueChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonWrapper}>
|
|
||||||
{isNew ? null : (
|
|
||||||
<IconButton
|
|
||||||
name={icons.REMOVE}
|
|
||||||
tabIndex={-1}
|
|
||||||
onPress={handleRemovePress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyValueListInputItem;
|
|
|
@ -5,13 +5,13 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { metadataProfileNames } from 'Helpers/Props';
|
import { metadataProfileNames } from 'Helpers/Props';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.metadataProfiles', sortByProp('name')),
|
createSortedSectionSelector('settings.metadataProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
|
|
|
@ -25,7 +25,7 @@ function MonitorAlbumsSelectInput(props) {
|
||||||
if (includeMixed) {
|
if (includeMixed) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: `(${translate('Mixed')})`,
|
value: '(Mixed)',
|
||||||
isDisabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,6 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||||
return inputTypes.CHECK;
|
return inputTypes.CHECK;
|
||||||
case 'device':
|
case 'device':
|
||||||
return inputTypes.DEVICE;
|
return inputTypes.DEVICE;
|
||||||
case 'keyValueList':
|
|
||||||
return inputTypes.KEY_VALUE_LIST;
|
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
return inputTypes.PLAYLIST;
|
return inputTypes.PLAYLIST;
|
||||||
case 'password':
|
case 'password':
|
||||||
|
|
|
@ -4,13 +4,13 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
|
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
|
|
|
@ -52,7 +52,6 @@ class SelectInput extends Component {
|
||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
value: optionValue,
|
value: optionValue,
|
||||||
isDisabled: optionIsDisabled = false,
|
|
||||||
...otherOptionProps
|
...otherOptionProps
|
||||||
} = option;
|
} = option;
|
||||||
|
|
||||||
|
@ -60,7 +59,6 @@ class SelectInput extends Component {
|
||||||
<option
|
<option
|
||||||
key={key}
|
key={key}
|
||||||
value={key}
|
value={key}
|
||||||
disabled={optionIsDisabled}
|
|
||||||
{...otherOptionProps}
|
{...otherOptionProps}
|
||||||
>
|
>
|
||||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterMenuItem from './FilterMenuItem';
|
import FilterMenuItem from './FilterMenuItem';
|
||||||
import MenuContent from './MenuContent';
|
import MenuContent from './MenuContent';
|
||||||
|
@ -48,7 +47,7 @@ class FilterMenuContent extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort(sortByProp('label'))
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
.map((filter) => {
|
.map((filter) => {
|
||||||
return (
|
return (
|
||||||
<FilterMenuItem
|
<FilterMenuItem
|
||||||
|
|
|
@ -83,6 +83,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
|
.modal.small,
|
||||||
|
.modal.medium {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.modalContainer {
|
.modalContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
&.isDisabled {
|
.isDisabled {
|
||||||
color: var(--disabledColor);
|
color: var(--disabledColor);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
import ArtistSearchInputConnector from './ArtistSearchInputConnector';
|
||||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||||
import styles from './PageHeader.css';
|
import styles from './PageHeader.css';
|
||||||
|
|
||||||
class PageHeader extends Component {
|
class PageHeader extends Component {
|
||||||
|
@ -83,7 +83,6 @@ class PageHeader extends Component {
|
||||||
size={14}
|
size={14}
|
||||||
title={translate('Donate')}
|
title={translate('Donate')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.translation}
|
className={styles.translation}
|
||||||
title={translate('SuggestTranslationChange')}
|
title={translate('SuggestTranslationChange')}
|
||||||
|
@ -91,8 +90,7 @@ class PageHeader extends Component {
|
||||||
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
to="https://translate.servarr.com/projects/servarr/lidarr/"
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
|
<PageHeaderActionsMenuConnector
|
||||||
<PageHeaderActionsMenu
|
|
||||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
90
frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
Normal file
90
frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
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;
|
|
@ -1,87 +0,0 @@
|
||||||
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;
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
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);
|
|
@ -172,7 +172,7 @@ class SignalRConnector extends Component {
|
||||||
const status = resource.status;
|
const status = resource.status;
|
||||||
|
|
||||||
// Both successful and failed commands need to be
|
// Both successful and failed commands need to be
|
||||||
// completed, otherwise they spin until they time out.
|
// completed, otherwise they spin until they timeout.
|
||||||
|
|
||||||
if (status === 'completed' || status === 'failed') {
|
if (status === 'completed' || status === 'failed') {
|
||||||
this.props.dispatchFinishCommand(resource);
|
this.props.dispatchFinishCommand(resource);
|
||||||
|
@ -224,58 +224,10 @@ class SignalRConnector extends Component {
|
||||||
repopulatePage('trackFileUpdated');
|
repopulatePage('trackFileUpdated');
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDownloadclient = ({ action, resource }) => {
|
|
||||||
const section = 'settings.downloadClients';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleHealth = () => {
|
handleHealth = () => {
|
||||||
this.props.dispatchFetchHealth();
|
this.props.dispatchFetchHealth();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleImportlist = ({ action, resource }) => {
|
|
||||||
const section = 'settings.importLists';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleIndexer = ({ action, resource }) => {
|
|
||||||
const section = 'settings.indexers';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMetadata = ({ action, resource }) => {
|
|
||||||
const section = 'settings.metadata';
|
|
||||||
|
|
||||||
if (action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNotification = ({ action, resource }) => {
|
|
||||||
const section = 'settings.notifications';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleArtist = (body) => {
|
handleArtist = (body) => {
|
||||||
const action = body.action;
|
const action = body.action;
|
||||||
const section = 'artist';
|
const section = 'artist';
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
line-height: 1.52857143;
|
line-height: 1.52857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.pager {
|
.pager {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
import styles from './TagList.css';
|
import styles from './TagList.css';
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ function TagList({ tags, tagList }) {
|
||||||
const sortedTags = tags
|
const sortedTags = tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((tag) => !!tag)
|
.filter((tag) => !!tag)
|
||||||
.sort(sortByProp('label'));
|
.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
|
|
9
frontend/src/Content/Images/Icons/browserconfig.xml
Normal file
9
frontend/src/Content/Images/Icons/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
|
||||||
|
<TileColor>#00ccff</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
19
frontend/src/Content/Images/Icons/manifest.json
Normal file
19
frontend/src/Content/Images/Icons/manifest.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "__INSTANCE_NAME__",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "__URL_BASE__/",
|
|
||||||
"theme_color": "#3a3f51",
|
|
||||||
"background_color": "#3a3f51",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
|
@ -2,8 +2,8 @@ export const AUTO_COMPLETE = 'autoComplete';
|
||||||
export const CAPTCHA = 'captcha';
|
export const CAPTCHA = 'captcha';
|
||||||
export const CHECK = 'check';
|
export const CHECK = 'check';
|
||||||
export const DEVICE = 'device';
|
export const DEVICE = 'device';
|
||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
|
||||||
export const PLAYLIST = 'playlist';
|
export const PLAYLIST = 'playlist';
|
||||||
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
|
||||||
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
||||||
export const FLOAT = 'float';
|
export const FLOAT = 'float';
|
||||||
|
@ -34,8 +34,8 @@ export const all = [
|
||||||
CAPTCHA,
|
CAPTCHA,
|
||||||
CHECK,
|
CHECK,
|
||||||
DEVICE,
|
DEVICE,
|
||||||
KEY_VALUE_LIST,
|
|
||||||
PLAYLIST,
|
PLAYLIST,
|
||||||
|
KEY_VALUE_LIST,
|
||||||
MONITOR_ALBUMS_SELECT,
|
MONITOR_ALBUMS_SELECT,
|
||||||
MONITOR_NEW_ITEMS_SELECT,
|
MONITOR_NEW_ITEMS_SELECT,
|
||||||
FLOAT,
|
FLOAT,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import Scroller from 'Components/Scroller/Scroller';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectAlbumRow from './SelectAlbumRow';
|
import SelectAlbumRow from './SelectAlbumRow';
|
||||||
import styles from './SelectAlbumModalContent.css';
|
import styles from './SelectAlbumModalContent.css';
|
||||||
|
@ -20,7 +19,6 @@ const columns = [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
label: () => translate('AlbumTitle'),
|
label: () => translate('AlbumTitle'),
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -31,7 +29,6 @@ const columns = [
|
||||||
{
|
{
|
||||||
name: 'releaseDate',
|
name: 'releaseDate',
|
||||||
label: () => translate('ReleaseDate'),
|
label: () => translate('ReleaseDate'),
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -66,22 +63,16 @@ class SelectAlbumModalContent extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
items,
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
onSortPress,
|
|
||||||
onAlbumSelect,
|
onAlbumSelect,
|
||||||
onModalClose
|
onModalClose,
|
||||||
|
isFetching,
|
||||||
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const filter = this.state.filter;
|
const filter = this.state.filter;
|
||||||
const filterLower = filter.toLowerCase();
|
const filterLower = filter.toLowerCase();
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error, 'Unable to load albums');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
|
@ -92,14 +83,10 @@ class SelectAlbumModalContent extends Component {
|
||||||
className={styles.modalBody}
|
className={styles.modalBody}
|
||||||
scrollDirection={scrollDirections.NONE}
|
scrollDirection={scrollDirections.NONE}
|
||||||
>
|
>
|
||||||
<Scroller
|
{
|
||||||
className={styles.scroller}
|
isFetching &&
|
||||||
autoFocus={false}
|
<LoadingIndicator />
|
||||||
>
|
}
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
className={styles.filterInput}
|
className={styles.filterInput}
|
||||||
placeholder={translate('FilterAlbumPlaceholder')}
|
placeholder={translate('FilterAlbumPlaceholder')}
|
||||||
|
@ -109,12 +96,14 @@ class SelectAlbumModalContent extends Component {
|
||||||
onChange={this.onFilterChange}
|
onChange={this.onFilterChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isPopulated && !!items.length ? (
|
<Scroller
|
||||||
|
className={styles.scroller}
|
||||||
|
autoFocus={false}
|
||||||
|
>
|
||||||
|
{
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sortKey={sortKey}
|
{...otherProps}
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{
|
{
|
||||||
|
@ -133,7 +122,7 @@ class SelectAlbumModalContent extends Component {
|
||||||
}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
) : null}
|
}
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
|
@ -148,13 +137,8 @@ class SelectAlbumModalContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectAlbumModalContent.propTypes = {
|
SelectAlbumModalContent.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
sortDirection: PropTypes.string,
|
|
||||||
onSortPress: PropTypes.func.isRequired,
|
|
||||||
onAlbumSelect: PropTypes.func.isRequired,
|
onAlbumSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,14 +3,18 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions';
|
import {
|
||||||
import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
clearInteractiveImportAlbums,
|
||||||
|
fetchInteractiveImportAlbums,
|
||||||
|
saveInteractiveImportItem,
|
||||||
|
setInteractiveImportAlbumsSort,
|
||||||
|
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
import SelectAlbumModalContent from './SelectAlbumModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createClientSideCollectionSelector('albumSelection'),
|
createClientSideCollectionSelector('interactiveImport.albums'),
|
||||||
(albums) => {
|
(albums) => {
|
||||||
return albums;
|
return albums;
|
||||||
}
|
}
|
||||||
|
@ -18,9 +22,9 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchAlbums,
|
fetchInteractiveImportAlbums,
|
||||||
setAlbumsSort,
|
setInteractiveImportAlbumsSort,
|
||||||
clearAlbums,
|
clearInteractiveImportAlbums,
|
||||||
updateInteractiveImportItem,
|
updateInteractiveImportItem,
|
||||||
saveInteractiveImportItem
|
saveInteractiveImportItem
|
||||||
};
|
};
|
||||||
|
@ -35,20 +39,20 @@ class SelectAlbumModalContentConnector extends Component {
|
||||||
artistId
|
artistId
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
this.props.fetchAlbums({ artistId });
|
this.props.fetchInteractiveImportAlbums({ artistId });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// This clears the albums for the queue and hides the queue
|
// This clears the albums for the queue and hides the queue
|
||||||
// We'll need another place to store albums for manual import
|
// We'll need another place to store albums for manual import
|
||||||
this.props.clearAlbums();
|
this.props.clearInteractiveImportAlbums();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onSortPress = (sortKey, sortDirection) => {
|
onSortPress = (sortKey, sortDirection) => {
|
||||||
this.props.setAlbumsSort({ sortKey, sortDirection });
|
this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
|
||||||
};
|
};
|
||||||
|
|
||||||
onAlbumSelect = (albumId) => {
|
onAlbumSelect = (albumId) => {
|
||||||
|
@ -78,7 +82,6 @@ class SelectAlbumModalContentConnector extends Component {
|
||||||
return (
|
return (
|
||||||
<SelectAlbumModalContent
|
<SelectAlbumModalContent
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onAlbumSelect={this.onAlbumSelect}
|
onAlbumSelect={this.onAlbumSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -89,9 +92,9 @@ SelectAlbumModalContentConnector.propTypes = {
|
||||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
artistId: PropTypes.number.isRequired,
|
artistId: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
fetchAlbums: PropTypes.func.isRequired,
|
fetchInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||||
setAlbumsSort: PropTypes.func.isRequired,
|
setInteractiveImportAlbumsSort: PropTypes.func.isRequired,
|
||||||
clearAlbums: PropTypes.func.isRequired,
|
clearInteractiveImportAlbums: PropTypes.func.isRequired,
|
||||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|
|
@ -18,17 +18,12 @@
|
||||||
.leftButtons,
|
.leftButtons,
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 0 50%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftButtons {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex: 1 1 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.importMode,
|
.importMode,
|
||||||
|
@ -36,7 +31,6 @@
|
||||||
composes: select from '~Components/Form/SelectInput.css';
|
composes: select from '~Components/Form/SelectInput.css';
|
||||||
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
max-width: 100%;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,12 +43,10 @@
|
||||||
.leftButtons,
|
.leftButtons,
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftButtons {
|
.leftButtons {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
max-width: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightButtons {
|
.rightButtons {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
@ -131,8 +130,7 @@ class AddNewItem extends Component {
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
|
<div>{getErrorMessage(error)}</div>
|
||||||
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
@ -8,7 +8,6 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||||
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
|
|
||||||
|
|
||||||
function CustomFormatSettingsPage() {
|
function CustomFormatSettingsPage() {
|
||||||
return (
|
return (
|
||||||
|
@ -18,13 +17,11 @@ function CustomFormatSettingsPage() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
showSave={false}
|
showSave={false}
|
||||||
additionalButtons={
|
additionalButtons={
|
||||||
<>
|
<Fragment>
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<ParseToolbarButton />
|
<ParseToolbarButton />
|
||||||
|
</Fragment>
|
||||||
<ManageCustomFormatsToolbarButton />
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions';
|
import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import CustomFormats from './CustomFormats';
|
import CustomFormats from './CustomFormats';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.customFormats', sortByProp('name')),
|
createSortedSectionSelector('settings.customFormats', sortByName),
|
||||||
(customFormats) => customFormats
|
(customFormats) => customFormats
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||||
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
|
||||||
|
|
||||||
function mapStateToProps() {
|
function mapStateToProps() {
|
||||||
return {};
|
return {};
|
||||||
|
@ -37,7 +36,6 @@ class EditCustomFormatModalConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
EditCustomFormatModalConnector.propTypes = {
|
EditCustomFormatModalConnector.propTypes = {
|
||||||
...EditCustomFormatModalContentConnector.propTypes,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,8 +25,3 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--cardCenterBackgroundColor);
|
background-color: var(--cardCenterBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customFormats {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'addSpecification': string;
|
'addSpecification': string;
|
||||||
'center': string;
|
'center': string;
|
||||||
'customFormats': string;
|
|
||||||
'deleteButton': string;
|
'deleteButton': string;
|
||||||
'rightButtons': string;
|
'rightButtons': string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
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;
|
|
|
@ -1,16 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'modalFooter': string;
|
|
||||||
'selected': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,125 +0,0 @@
|
||||||
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;
|
|
|
@ -1,20 +0,0 @@
|
||||||
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;
|
|
|
@ -1,16 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// 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;
|
|
|
@ -1,244 +0,0 @@
|
||||||
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;
|
|
|
@ -1,12 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// 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;
|
|
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