mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 13:10:13 -07:00
Compare commits
No commits in common. "develop" and "v2.5.3.4341" have entirely different histories.
develop
...
v2.5.3.434
374 changed files with 5556 additions and 10175 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
|
||||||
|
|
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.3'
|
majorVersion: '2.5.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.424'
|
||||||
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'
|
||||||
|
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,7 +20,6 @@ interface Album extends ModelBase {
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
lastSearchTime?: string;
|
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)}
|
{
|
||||||
</span>
|
formatBytes(sizeOnDisk || 0)
|
||||||
</div>
|
}
|
||||||
|
</span>
|
||||||
</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}>
|
|
||||||
{albumType}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
<span className={styles.qualityProfileName}>
|
||||||
secondaryTypes.length ?
|
{albumType}
|
||||||
<Label
|
</span>
|
||||||
className={styles.detailsLabel}
|
</Label>
|
||||||
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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -13,16 +13,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,
|
||||||
|
@ -62,7 +59,6 @@ interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
advancedSettings: boolean;
|
||||||
customFormats: CustomFormatAppState;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
get name() {
|
get name() {
|
||||||
return translate('ImportCompleteFailed');
|
return translate('ImportFailed');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
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;
|
|
|
@ -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':
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
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,29 +83,27 @@ class SelectAlbumModalContent extends Component {
|
||||||
className={styles.modalBody}
|
className={styles.modalBody}
|
||||||
scrollDirection={scrollDirections.NONE}
|
scrollDirection={scrollDirections.NONE}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
<TextInput
|
||||||
|
className={styles.filterInput}
|
||||||
|
placeholder={translate('FilterAlbumPlaceholder')}
|
||||||
|
name="filter"
|
||||||
|
value={filter}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={this.onFilterChange}
|
||||||
|
/>
|
||||||
|
|
||||||
<Scroller
|
<Scroller
|
||||||
className={styles.scroller}
|
className={styles.scroller}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
>
|
>
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
{
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
className={styles.filterInput}
|
|
||||||
placeholder={translate('FilterAlbumPlaceholder')}
|
|
||||||
name="filter"
|
|
||||||
value={filter}
|
|
||||||
autoFocus={true}
|
|
||||||
onChange={this.onFilterChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isPopulated && !!items.length ? (
|
|
||||||
<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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteCustomFormats,
|
bulkDeleteCustomFormats,
|
||||||
bulkEditCustomFormats,
|
bulkEditCustomFormats,
|
||||||
|
@ -34,7 +34,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
typeof ManageCustomFormatsModalRow
|
typeof ManageCustomFormatsModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
|
@ -47,15 +47,12 @@ const COLUMNS: Column[] = [
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
label: '',
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ManageCustomFormatsModalContentProps {
|
interface ManageCustomFormatsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageCustomFormatsModalContent(
|
function ManageCustomFormatsModalContent(
|
||||||
|
|
|
@ -4,9 +4,3 @@
|
||||||
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
|
||||||
'includeCustomFormatWhenRenaming': string;
|
'includeCustomFormatWhenRenaming': string;
|
||||||
'name': string;
|
'name': string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import { deleteCustomFormat } from 'Store/Actions/settingsActions';
|
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector';
|
|
||||||
import styles from './ManageCustomFormatsModalRow.css';
|
import styles from './ManageCustomFormatsModalRow.css';
|
||||||
|
|
||||||
interface ManageCustomFormatsModalRowProps {
|
interface ManageCustomFormatsModalRowProps {
|
||||||
|
@ -24,15 +16,6 @@ interface ManageCustomFormatsModalRowProps {
|
||||||
onSelectedChange(result: SelectStateInputProps): void;
|
onSelectedChange(result: SelectStateInputProps): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDeletingSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.settings.customFormats.isDeleting,
|
|
||||||
(isDeleting) => {
|
|
||||||
return isDeleting;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
@ -42,16 +25,7 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
||||||
onSelectedChange,
|
onSelectedChange,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const onSelectedChangeWrapper = useCallback(
|
||||||
const isDeleting = useSelector(isDeletingSelector());
|
|
||||||
|
|
||||||
const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const handlelectedChange = useCallback(
|
|
||||||
(result: SelectStateInputProps) => {
|
(result: SelectStateInputProps) => {
|
||||||
onSelectedChange({
|
onSelectedChange({
|
||||||
...result,
|
...result,
|
||||||
|
@ -60,33 +34,12 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
||||||
[onSelectedChange]
|
[onSelectedChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEditCustomFormatModalOpen = useCallback(() => {
|
|
||||||
setIsEditCustomFormatModalOpen(true);
|
|
||||||
}, [setIsEditCustomFormatModalOpen]);
|
|
||||||
|
|
||||||
const handleEditCustomFormatModalClose = useCallback(() => {
|
|
||||||
setIsEditCustomFormatModalOpen(false);
|
|
||||||
}, [setIsEditCustomFormatModalOpen]);
|
|
||||||
|
|
||||||
const handleDeleteCustomFormatPress = useCallback(() => {
|
|
||||||
setIsEditCustomFormatModalOpen(false);
|
|
||||||
setIsDeleteCustomFormatModalOpen(true);
|
|
||||||
}, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]);
|
|
||||||
|
|
||||||
const handleDeleteCustomFormatModalClose = useCallback(() => {
|
|
||||||
setIsDeleteCustomFormatModalOpen(false);
|
|
||||||
}, [setIsDeleteCustomFormatModalOpen]);
|
|
||||||
|
|
||||||
const handleConfirmDeleteCustomFormat = useCallback(() => {
|
|
||||||
dispatch(deleteCustomFormat({ id }));
|
|
||||||
}, [id, dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableSelectCell
|
<TableSelectCell
|
||||||
id={id}
|
id={id}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onSelectedChange={handlelectedChange}
|
onSelectedChange={onSelectedChangeWrapper}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
||||||
|
@ -94,31 +47,6 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
||||||
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
|
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
|
||||||
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
|
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
<IconButton
|
|
||||||
name={icons.EDIT}
|
|
||||||
onPress={handleEditCustomFormatModalOpen}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<EditCustomFormatModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={isEditCustomFormatModalOpen}
|
|
||||||
onModalClose={handleEditCustomFormatModalClose}
|
|
||||||
onDeleteCustomFormatPress={handleDeleteCustomFormatPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isDeleteCustomFormatModalOpen}
|
|
||||||
kind="danger"
|
|
||||||
title={translate('DeleteCustomFormat')}
|
|
||||||
message={translate('DeleteCustomFormatMessageText', { name })}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
isSpinning={isDeleting}
|
|
||||||
onConfirm={handleConfirmDeleteCustomFormat}
|
|
||||||
onCancel={handleDeleteCustomFormatModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ function ManageCustomFormatsToolbarButton() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('ManageFormats')}
|
label={translate('ManageCustomFormats')}
|
||||||
iconName={icons.MANAGE}
|
iconName={icons.MANAGE}
|
||||||
onPress={openManageModal}
|
onPress={openManageModal}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,8 +7,8 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
@ -52,13 +52,12 @@ function EditSpecificationModalContent(props) {
|
||||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
|
||||||
|
{'More details'} <Link to="https://www.regular-expressions.info/tutorial.html">{'Here'}</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
{'Regular expressions can be tested '}
|
||||||
</div>
|
<Link to="http://regexstorm.net/tester">Here</Link>
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteDownloadClients,
|
bulkDeleteDownloadClients,
|
||||||
bulkEditDownloadClients,
|
bulkEditDownloadClients,
|
||||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
typeof ManageDownloadClientsModalRow
|
typeof ManageDownloadClientsModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
|
@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
|
|
|
@ -292,7 +292,7 @@ function EditImportListModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('ImportListTagsHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteIndexers,
|
bulkDeleteIndexers,
|
||||||
bulkEditIndexers,
|
bulkEditIndexers,
|
||||||
|
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
typeof ManageIndexersModalRow
|
typeof ManageIndexersModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
|
@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
|
||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
|
|
@ -191,21 +191,26 @@ class MediaManagement extends Component {
|
||||||
<FieldSet
|
<FieldSet
|
||||||
legend={translate('Importing')}
|
legend={translate('Importing')}
|
||||||
>
|
>
|
||||||
<FormGroup
|
{
|
||||||
advancedSettings={advancedSettings}
|
!isWindows &&
|
||||||
isAdvanced={true}
|
<FormGroup
|
||||||
size={sizes.MEDIUM}
|
advancedSettings={advancedSettings}
|
||||||
>
|
isAdvanced={true}
|
||||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('SkipFreeSpaceCheck')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipFreeSpaceCheckWhenImporting"
|
name="skipFreeSpaceCheckWhenImporting"
|
||||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
}
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -14,11 +15,11 @@ function createMapStateToProps() {
|
||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
(state) => state.settings.namingExamples,
|
(state) => state.settings.namingExamples,
|
||||||
createSettingsSectionSelector(SECTION),
|
createSettingsSectionSelector(SECTION),
|
||||||
(advancedSettings, namingExamples, sectionSettings) => {
|
(advancedSettings, examples, sectionSettings) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
examples: namingExamples.item,
|
examples: examples.item,
|
||||||
examplesPopulated: namingExamples.isPopulated,
|
examplesPopulated: !_.isEmpty(examples.item),
|
||||||
...sectionSettings
|
...sectionSettings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,9 +94,9 @@ class RootFolder extends Component {
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={this.state.isDeleteRootFolderModalOpen}
|
isOpen={this.state.isDeleteRootFolderModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('RemoveRootFolder')}
|
title={translate('DeleteRootFolder')}
|
||||||
message={translate('RemoveRootFolderArtistsMessageText', { name })}
|
message={translate('DeleteRootFolderMessageText', { name })}
|
||||||
confirmLabel={translate('Remove')}
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={this.onConfirmDeleteRootFolder}
|
onConfirm={this.onConfirmDeleteRootFolder}
|
||||||
onCancel={this.onDeleteRootFolderModalClose}
|
onCancel={this.onDeleteRootFolderModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -105,7 +105,7 @@ function EditNotificationModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('NotificationsTagsArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error ?
|
!isFetching && !!error ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<div>
|
||||||
{translate('AddDelayProfileError')}
|
{translate('UnableToAddANewQualityProfilePleaseTryAgain')}
|
||||||
</Alert> :
|
</div> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
{
|
{
|
||||||
id === 1 ?
|
id === 1 ?
|
||||||
<Alert>
|
<Alert>
|
||||||
{translate('DefaultDelayProfileArtist')}
|
{translate('DefaultDelayProfileHelpText')}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
{...tags}
|
{...tags}
|
||||||
helpText={translate('DelayProfileArtistTagsHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('ReleaseProfileTagArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -24,19 +24,19 @@
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.bar {
|
||||||
top: 9px;
|
top: 9px;
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background-color: var(--sliderAccentColor);
|
background-color: var(--sliderAccentColor);
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
|
|
||||||
&:nth-child(3n + 1) {
|
&:nth-child(3n+1) {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.handle {
|
||||||
top: 1px;
|
top: 1px;
|
||||||
z-index: 0 !important;
|
z-index: 0 !important;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'bar': string;
|
||||||
|
'handle': string;
|
||||||
'kilobitsPerSecond': string;
|
'kilobitsPerSecond': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
'qualityDefinition': string;
|
'qualityDefinition': string;
|
||||||
|
@ -8,9 +10,7 @@ interface CssExports {
|
||||||
'sizeLimit': string;
|
'sizeLimit': string;
|
||||||
'sizes': string;
|
'sizes': string;
|
||||||
'slider': string;
|
'slider': string;
|
||||||
'thumb': string;
|
|
||||||
'title': string;
|
'title': string;
|
||||||
'track': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -55,27 +55,6 @@ class QualityDefinition extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
trackRenderer(props, state) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={styles.track}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbRenderer(props, state) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={styles.thumb}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
@ -195,7 +174,6 @@ class QualityDefinition extends Component {
|
||||||
|
|
||||||
<div className={styles.sizeLimit}>
|
<div className={styles.sizeLimit}>
|
||||||
<ReactSlider
|
<ReactSlider
|
||||||
className={styles.slider}
|
|
||||||
min={slider.min}
|
min={slider.min}
|
||||||
max={slider.max}
|
max={slider.max}
|
||||||
step={slider.step}
|
step={slider.step}
|
||||||
|
@ -204,9 +182,9 @@ class QualityDefinition extends Component {
|
||||||
withTracks={true}
|
withTracks={true}
|
||||||
allowCross={false}
|
allowCross={false}
|
||||||
snapDragDisabled={true}
|
snapDragDisabled={true}
|
||||||
pearling={true}
|
className={styles.slider}
|
||||||
renderThumb={this.thumbRenderer}
|
trackClassName={styles.bar}
|
||||||
renderTrack={this.trackRenderer}
|
thumbClassName={styles.handle}
|
||||||
onChange={this.onSliderChange}
|
onChange={this.onSliderChange}
|
||||||
onAfterChange={this.onAfterSliderChange}
|
onAfterChange={this.onAfterSliderChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
|
||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,11 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import Tags from './Tags';
|
import Tags from './Tags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('tags', sortByProp('label')),
|
(state) => state.tags,
|
||||||
(tags) => {
|
(tags) => {
|
||||||
const isFetching = tags.isFetching || tags.details.isFetching;
|
const isFetching = tags.isFetching || tags.details.isFetching;
|
||||||
const error = tags.error || tags.details.error;
|
const error = tags.error || tags.details.error;
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
import moment from 'moment';
|
|
||||||
import { createAction } from 'redux-actions';
|
|
||||||
import { sortDirections } from 'Helpers/Props';
|
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
export const section = 'albumSelection';
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
isFetching: false,
|
|
||||||
isReprocessing: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
sortKey: 'title',
|
|
||||||
sortDirection: sortDirections.ASCENDING,
|
|
||||||
items: [],
|
|
||||||
sortPredicates: {
|
|
||||||
title: ({ title }) => {
|
|
||||||
return title.toLocaleLowerCase();
|
|
||||||
},
|
|
||||||
|
|
||||||
releaseDate: function({ releaseDate }, direction) {
|
|
||||||
if (releaseDate) {
|
|
||||||
return moment(releaseDate).unix();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === sortDirections.DESCENDING) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Number.MAX_VALUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const persistState = [
|
|
||||||
'albumSelection.sortKey',
|
|
||||||
'albumSelection.sortDirection'
|
|
||||||
];
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const FETCH_ALBUMS = 'albumSelection/fetchAlbums';
|
|
||||||
export const SET_ALBUMS_SORT = 'albumSelection/setAlbumsSort';
|
|
||||||
export const CLEAR_ALBUMS = 'albumSelection/clearAlbums';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const fetchAlbums = createThunk(FETCH_ALBUMS);
|
|
||||||
export const setAlbumsSort = createAction(SET_ALBUMS_SORT);
|
|
||||||
export const clearAlbums = createAction(CLEAR_ALBUMS);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
[FETCH_ALBUMS]: createFetchHandler(section, '/album')
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
|
|
||||||
export const reducers = createHandleActions({
|
|
||||||
|
|
||||||
[SET_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(section),
|
|
||||||
|
|
||||||
[CLEAR_ALBUMS]: (state) => {
|
|
||||||
return updateSectionState(state, section, {
|
|
||||||
...defaultState,
|
|
||||||
sortKey: state.sortKey,
|
|
||||||
sortDirection: state.sortDirection
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}, defaultState, section);
|
|
|
@ -151,7 +151,7 @@ export const defaultState = {
|
||||||
{
|
{
|
||||||
name: 'genres',
|
name: 'genres',
|
||||||
label: () => translate('Genres'),
|
label: () => translate('Genres'),
|
||||||
isSortable: true,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -150,7 +150,7 @@ export const defaultState = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'importFailed',
|
key: 'importFailed',
|
||||||
label: () => translate('ImportCompleteFailed'),
|
label: () => translate('ImportFailed'),
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'eventType',
|
key: 'eventType',
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as albums from './albumActions';
|
import * as albums from './albumActions';
|
||||||
import * as albumHistory from './albumHistoryActions';
|
import * as albumHistory from './albumHistoryActions';
|
||||||
import * as albumSelection from './albumSelectionActions';
|
|
||||||
import * as app from './appActions';
|
import * as app from './appActions';
|
||||||
import * as artist from './artistActions';
|
import * as artist from './artistActions';
|
||||||
import * as artistHistory from './artistHistoryActions';
|
import * as artistHistory from './artistHistoryActions';
|
||||||
|
@ -30,18 +29,14 @@ import * as wanted from './wantedActions';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
app,
|
app,
|
||||||
albums,
|
|
||||||
albumHistory,
|
|
||||||
albumSelection,
|
|
||||||
artist,
|
|
||||||
artistHistory,
|
|
||||||
artistIndex,
|
|
||||||
blocklist,
|
blocklist,
|
||||||
captcha,
|
captcha,
|
||||||
calendar,
|
calendar,
|
||||||
commands,
|
commands,
|
||||||
customFilters,
|
customFilters,
|
||||||
|
albums,
|
||||||
trackFiles,
|
trackFiles,
|
||||||
|
albumHistory,
|
||||||
history,
|
history,
|
||||||
interactiveImportActions,
|
interactiveImportActions,
|
||||||
oAuth,
|
oAuth,
|
||||||
|
@ -52,6 +47,9 @@ export default [
|
||||||
providerOptions,
|
providerOptions,
|
||||||
queue,
|
queue,
|
||||||
releases,
|
releases,
|
||||||
|
artist,
|
||||||
|
artistHistory,
|
||||||
|
artistIndex,
|
||||||
search,
|
search,
|
||||||
settings,
|
settings,
|
||||||
system,
|
system,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create
|
||||||
|
|
||||||
export const section = 'interactiveImport';
|
export const section = 'interactiveImport';
|
||||||
|
|
||||||
|
const albumsSection = `${section}.albums`;
|
||||||
const trackFilesSection = `${section}.trackFiles`;
|
const trackFilesSection = `${section}.trackFiles`;
|
||||||
let abortCurrentFetchRequest = null;
|
let abortCurrentFetchRequest = null;
|
||||||
let abortCurrentRequest = null;
|
let abortCurrentRequest = null;
|
||||||
|
@ -57,6 +58,15 @@ export const defaultState = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
albums: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
sortKey: 'albumTitle',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
items: []
|
||||||
|
},
|
||||||
|
|
||||||
trackFiles: {
|
trackFiles: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
|
@ -87,6 +97,10 @@ export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
|
||||||
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
||||||
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
||||||
|
|
||||||
|
export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/fetchInteractiveImportAlbums';
|
||||||
|
export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'interactiveImport/clearInteractiveImportAlbumsSort';
|
||||||
|
export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/clearInteractiveImportAlbums';
|
||||||
|
|
||||||
export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles';
|
export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles';
|
||||||
export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles';
|
export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles';
|
||||||
|
|
||||||
|
@ -103,6 +117,10 @@ export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
|
||||||
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
||||||
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
||||||
|
|
||||||
|
export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT_ALBUMS);
|
||||||
|
export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT);
|
||||||
|
export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS);
|
||||||
|
|
||||||
export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES);
|
export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES);
|
||||||
export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES);
|
export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES);
|
||||||
|
|
||||||
|
@ -235,6 +253,8 @@ export const actionHandlers = handleThunks({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'),
|
||||||
|
|
||||||
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
|
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -316,6 +336,14 @@ export const reducers = createHandleActions({
|
||||||
return Object.assign({}, state, { importMode: payload.importMode });
|
return Object.assign({}, state, { importMode: payload.importMode });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(albumsSection),
|
||||||
|
|
||||||
|
[CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => {
|
||||||
|
return updateSectionState(state, albumsSection, {
|
||||||
|
...defaultState.albums
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
[CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => {
|
[CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => {
|
||||||
return updateSectionState(state, trackFilesSection, {
|
return updateSectionState(state, trackFilesSection, {
|
||||||
...defaultState.trackFiles
|
...defaultState.trackFiles
|
||||||
|
|
|
@ -52,12 +52,6 @@ export const defaultState = {
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'albums.lastSearchTime',
|
|
||||||
label: () => translate('LastSearched'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
// {
|
// {
|
||||||
// name: 'status',
|
// name: 'status',
|
||||||
// label: 'Status',
|
// label: 'Status',
|
||||||
|
@ -137,12 +131,6 @@ export const defaultState = {
|
||||||
// label: 'Status',
|
// label: 'Status',
|
||||||
// isVisible: true
|
// isVisible: true
|
||||||
// },
|
// },
|
||||||
{
|
|
||||||
name: 'albums.lastSearchTime',
|
|
||||||
label: () => translate('LastSearched'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
columnLabel: () => translate('Actions'),
|
columnLabel: () => translate('Actions'),
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AlbumAppState from 'App/State/AlbumAppState';
|
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import { createArtistSelectorForHook } from './createArtistSelector';
|
import { createArtistSelectorForHook } from './createArtistSelector';
|
||||||
|
@ -8,7 +7,7 @@ function createArtistAlbumsSelector(artistId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.albums,
|
(state: AppState) => state.albums,
|
||||||
createArtistSelectorForHook(artistId),
|
createArtistSelectorForHook(artistId),
|
||||||
(albums: AlbumAppState, artist = {} as Artist) => {
|
(albums, artist = {} as Artist) => {
|
||||||
const { isFetching, isPopulated, error, items } = albums;
|
const { isFetching, isPopulated, error, items } = albums;
|
||||||
|
|
||||||
const filteredAlbums = items.filter(
|
const filteredAlbums = items.filter(
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import MetadataProfile from 'typings/MetadataProfile';
|
|
||||||
import { createArtistSelectorForHook } from './createArtistSelector';
|
import { createArtistSelectorForHook } from './createArtistSelector';
|
||||||
|
|
||||||
function createArtistMetadataProfileSelector(artistId: number) {
|
function createArtistMetadataProfileSelector(artistId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings.metadataProfiles.items,
|
(state: AppState) => state.settings.metadataProfiles.items,
|
||||||
createArtistSelectorForHook(artistId),
|
createArtistSelectorForHook(artistId),
|
||||||
(metadataProfiles: MetadataProfile[], artist = {} as Artist) => {
|
(metadataProfiles, artist = {} as Artist) => {
|
||||||
return metadataProfiles.find((profile) => {
|
return metadataProfiles.find((profile) => {
|
||||||
return profile.id === artist.metadataProfileId;
|
return profile.id === artist.metadataProfileId;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
|
||||||
import { createArtistSelectorForHook } from './createArtistSelector';
|
import { createArtistSelectorForHook } from './createArtistSelector';
|
||||||
|
|
||||||
function createArtistQualityProfileSelector(artistId: number) {
|
function createArtistQualityProfileSelector(artistId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings.qualityProfiles.items,
|
(state: AppState) => state.settings.qualityProfiles.items,
|
||||||
createArtistSelectorForHook(artistId),
|
createArtistSelectorForHook(artistId),
|
||||||
(qualityProfiles: QualityProfile[], artist = {} as Artist) => {
|
(qualityProfiles, artist = {} as Artist) => {
|
||||||
return qualityProfiles.find(
|
return qualityProfiles.find(
|
||||||
(profile) => profile.id === artist.qualityProfileId
|
(profile) => profile.id === artist.qualityProfileId
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
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 Alert from 'Components/Alert';
|
||||||
|
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 PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
@ -77,16 +77,15 @@ class LogFiles extends Component {
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<Alert>
|
<Alert>
|
||||||
<div>
|
<div>
|
||||||
{translate('LogFilesLocation', {
|
Log files are located in: {location}
|
||||||
location
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentLogView === 'Log Files' ? (
|
{
|
||||||
<div>
|
currentLogView === 'Log Files' &&
|
||||||
<InlineMarkdown data={translate('TheLogLevelDefault')} />
|
<div>
|
||||||
</div>
|
The log level defaults to 'Info' and can be changed in <Link to="/settings/general">General Settings</Link>
|
||||||
) : null}
|
</div>
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
48
frontend/src/System/Updates/UpdateChanges.js
Normal file
48
frontend/src/System/Updates/UpdateChanges.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import styles from './UpdateChanges.css';
|
||||||
|
|
||||||
|
class UpdateChanges extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
changes
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (changes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueChanges = [...new Set(changes)];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
uniqueChanges.map((change, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
<InlineMarkdown data={change} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateChanges.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
changes: PropTypes.arrayOf(PropTypes.string)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateChanges;
|
|
@ -1,43 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import styles from './UpdateChanges.css';
|
|
||||||
|
|
||||||
interface UpdateChangesProps {
|
|
||||||
title: string;
|
|
||||||
changes: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateChanges(props: UpdateChangesProps) {
|
|
||||||
const { title, changes } = props;
|
|
||||||
|
|
||||||
if (changes.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueChanges = [...new Set(changes)];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.title}>{title}</div>
|
|
||||||
<ul>
|
|
||||||
{uniqueChanges.map((change, index) => {
|
|
||||||
const checkChange = change.replace(
|
|
||||||
/#\d{4,5}\b/g,
|
|
||||||
(match) =>
|
|
||||||
`[${match}](https://github.com/Lidarr/Lidarr/issues/${match.substring(
|
|
||||||
1
|
|
||||||
)})`
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
<InlineMarkdown data={checkChange} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UpdateChanges;
|
|
249
frontend/src/System/Updates/Updates.js
Normal file
249
frontend/src/System/Updates/Updates.js
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import formatDate from 'Utilities/Date/formatDate';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import UpdateChanges from './UpdateChanges';
|
||||||
|
import styles from './Updates.css';
|
||||||
|
|
||||||
|
class Updates extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currentVersion,
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
updatesError,
|
||||||
|
generalSettingsError,
|
||||||
|
items,
|
||||||
|
isInstallingUpdate,
|
||||||
|
updateMechanism,
|
||||||
|
updateMechanismMessage,
|
||||||
|
shortDateFormat,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
onInstallLatestPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const hasError = !!(updatesError || generalSettingsError);
|
||||||
|
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||||
|
const noUpdates = isPopulated && !hasError && !items.length;
|
||||||
|
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
|
||||||
|
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||||
|
|
||||||
|
const externalUpdaterPrefix = 'Unable to update Lidarr directly,';
|
||||||
|
const externalUpdaterMessages = {
|
||||||
|
external: 'Lidarr is configured to use an external update mechanism',
|
||||||
|
apt: 'use apt to install the update',
|
||||||
|
docker: 'update the docker container to receive the update'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Updates')}>
|
||||||
|
<PageContentBody>
|
||||||
|
{
|
||||||
|
!isPopulated && !hasError &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
noUpdates &&
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{translate('NoUpdatesAreAvailable')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasUpdateToInstall &&
|
||||||
|
<div className={styles.messageContainer}>
|
||||||
|
{
|
||||||
|
updateMechanism === 'builtIn' || updateMechanism === 'script' ?
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.updateAvailable}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isInstallingUpdate}
|
||||||
|
onPress={onInstallLatestPress}
|
||||||
|
>
|
||||||
|
Install Latest
|
||||||
|
</SpinnerButton> :
|
||||||
|
|
||||||
|
<Fragment>
|
||||||
|
<Icon
|
||||||
|
name={icons.WARNING}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.message}>
|
||||||
|
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
noUpdateToInstall &&
|
||||||
|
<div className={styles.messageContainer}>
|
||||||
|
<Icon
|
||||||
|
className={styles.upToDateIcon}
|
||||||
|
name={icons.CHECK_CIRCLE}
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
|
<div className={styles.message}>
|
||||||
|
The latest version of Lidarr is already installed
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasUpdates &&
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
items.map((update) => {
|
||||||
|
const hasChanges = !!update.changes;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={update.version}
|
||||||
|
className={styles.update}
|
||||||
|
>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.version}>{update.version}</div>
|
||||||
|
<div className={styles.space}>—</div>
|
||||||
|
<div
|
||||||
|
className={styles.date}
|
||||||
|
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{formatDate(update.releaseDate, shortDateFormat)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
update.branch === 'master' ?
|
||||||
|
null :
|
||||||
|
<Label
|
||||||
|
className={styles.label}
|
||||||
|
>
|
||||||
|
{update.branch}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
update.version === currentVersion ?
|
||||||
|
<Label
|
||||||
|
className={styles.label}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
Currently Installed
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
update.version !== currentVersion && update.installedOn ?
|
||||||
|
<Label
|
||||||
|
className={styles.label}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
Previously Installed
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
!hasChanges &&
|
||||||
|
<div>
|
||||||
|
{translate('MaintenanceRelease')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasChanges &&
|
||||||
|
<div className={styles.changes}>
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('New')}
|
||||||
|
changes={update.changes.new}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('Fixed')}
|
||||||
|
changes={update.changes.fixed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!updatesError &&
|
||||||
|
<div>
|
||||||
|
Failed to fetch updates
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!generalSettingsError &&
|
||||||
|
<div>
|
||||||
|
Failed to update settings
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Updates.propTypes = {
|
||||||
|
currentVersion: PropTypes.string.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
updatesError: PropTypes.object,
|
||||||
|
generalSettingsError: PropTypes.object,
|
||||||
|
items: PropTypes.array.isRequired,
|
||||||
|
isInstallingUpdate: PropTypes.bool.isRequired,
|
||||||
|
updateMechanism: PropTypes.string,
|
||||||
|
updateMechanismMessage: PropTypes.string,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
onInstallLatestPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Updates;
|
|
@ -1,303 +0,0 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
|
||||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import { UpdateMechanism } from 'typings/Settings/General';
|
|
||||||
import formatDate from 'Utilities/Date/formatDate';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import UpdateChanges from './UpdateChanges';
|
|
||||||
import styles from './Updates.css';
|
|
||||||
|
|
||||||
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
|
||||||
|
|
||||||
function createUpdatesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.system.updates,
|
|
||||||
(state: AppState) => state.settings.general,
|
|
||||||
(updates, generalSettings) => {
|
|
||||||
const { error: updatesError, items } = updates;
|
|
||||||
|
|
||||||
const isFetching = updates.isFetching || generalSettings.isFetching;
|
|
||||||
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
updatesError,
|
|
||||||
generalSettingsError: generalSettings.error,
|
|
||||||
items,
|
|
||||||
updateMechanism: generalSettings.item.updateMechanism,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Updates() {
|
|
||||||
const currentVersion = useSelector((state: AppState) => state.app.version);
|
|
||||||
const { packageUpdateMechanismMessage } = useSelector(
|
|
||||||
createSystemStatusSelector()
|
|
||||||
);
|
|
||||||
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
|
|
||||||
createUISettingsSelector()
|
|
||||||
);
|
|
||||||
const isInstallingUpdate = useSelector(
|
|
||||||
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
updatesError,
|
|
||||||
generalSettingsError,
|
|
||||||
items,
|
|
||||||
updateMechanism,
|
|
||||||
} = useSelector(createUpdatesSelector());
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
|
|
||||||
const hasError = !!(updatesError || generalSettingsError);
|
|
||||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
|
||||||
const noUpdates = isPopulated && !hasError && !items.length;
|
|
||||||
|
|
||||||
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
|
|
||||||
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
|
|
||||||
external: translate('ExternalUpdater'),
|
|
||||||
apt: translate('AptUpdater'),
|
|
||||||
docker: translate('DockerUpdater'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
|
|
||||||
const majorVersion = parseInt(
|
|
||||||
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
|
|
||||||
);
|
|
||||||
|
|
||||||
const latestVersion = items[0]?.version;
|
|
||||||
const latestMajorVersion = parseInt(
|
|
||||||
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isMajorUpdate: latestMajorVersion > majorVersion,
|
|
||||||
hasUpdateToInstall: items.some(
|
|
||||||
(update) => update.installable && update.latest
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}, [currentVersion, items]);
|
|
||||||
|
|
||||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
|
||||||
|
|
||||||
const handleInstallLatestPress = useCallback(() => {
|
|
||||||
if (isMajorUpdate) {
|
|
||||||
setIsMajorUpdateModalOpen(true);
|
|
||||||
} else {
|
|
||||||
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
|
|
||||||
}
|
|
||||||
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
|
|
||||||
|
|
||||||
const handleInstallLatestMajorVersionPress = useCallback(() => {
|
|
||||||
setIsMajorUpdateModalOpen(false);
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
executeCommand({
|
|
||||||
name: commandNames.APPLICATION_UPDATE,
|
|
||||||
installMajorUpdate: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [setIsMajorUpdateModalOpen, dispatch]);
|
|
||||||
|
|
||||||
const handleCancelMajorVersionPress = useCallback(() => {
|
|
||||||
setIsMajorUpdateModalOpen(false);
|
|
||||||
}, [setIsMajorUpdateModalOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchUpdates());
|
|
||||||
dispatch(fetchGeneralSettings());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Updates')}>
|
|
||||||
<PageContentBody>
|
|
||||||
{isPopulated || hasError ? null : <LoadingIndicator />}
|
|
||||||
|
|
||||||
{noUpdates ? (
|
|
||||||
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{hasUpdateToInstall ? (
|
|
||||||
<div className={styles.messageContainer}>
|
|
||||||
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
|
|
||||||
<SpinnerButton
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={isInstallingUpdate}
|
|
||||||
onPress={handleInstallLatestPress}
|
|
||||||
>
|
|
||||||
{translate('InstallLatest')}
|
|
||||||
</SpinnerButton>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
|
||||||
|
|
||||||
<div className={styles.message}>
|
|
||||||
{externalUpdaterPrefix}{' '}
|
|
||||||
<InlineMarkdown
|
|
||||||
data={
|
|
||||||
packageUpdateMechanismMessage ||
|
|
||||||
externalUpdaterMessages[updateMechanism] ||
|
|
||||||
externalUpdaterMessages.external
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isFetching ? (
|
|
||||||
<LoadingIndicator className={styles.loading} size={20} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{noUpdateToInstall && (
|
|
||||||
<div className={styles.messageContainer}>
|
|
||||||
<Icon
|
|
||||||
className={styles.upToDateIcon}
|
|
||||||
name={icons.CHECK_CIRCLE}
|
|
||||||
size={30}
|
|
||||||
/>
|
|
||||||
<div className={styles.message}>{translate('OnLatestVersion')}</div>
|
|
||||||
|
|
||||||
{isFetching && (
|
|
||||||
<LoadingIndicator className={styles.loading} size={20} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasUpdates && (
|
|
||||||
<div>
|
|
||||||
{items.map((update) => {
|
|
||||||
return (
|
|
||||||
<div key={update.version} className={styles.update}>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.version}>{update.version}</div>
|
|
||||||
<div className={styles.space}>—</div>
|
|
||||||
<div
|
|
||||||
className={styles.date}
|
|
||||||
title={formatDateTime(
|
|
||||||
update.releaseDate,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatDate(update.releaseDate, shortDateFormat)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{update.branch === 'master' ? null : (
|
|
||||||
<Label className={styles.label}>{update.branch}</Label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{update.version === currentVersion ? (
|
|
||||||
<Label
|
|
||||||
className={styles.label}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
title={formatDateTime(
|
|
||||||
update.installedOn,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{translate('CurrentlyInstalled')}
|
|
||||||
</Label>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{update.version !== currentVersion && update.installedOn ? (
|
|
||||||
<Label
|
|
||||||
className={styles.label}
|
|
||||||
kind={kinds.INVERSE}
|
|
||||||
title={formatDateTime(
|
|
||||||
update.installedOn,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{translate('PreviouslyInstalled')}
|
|
||||||
</Label>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{update.changes ? (
|
|
||||||
<div>
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('New')}
|
|
||||||
changes={update.changes.new}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('Fixed')}
|
|
||||||
changes={update.changes.fixed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>{translate('MaintenanceRelease')}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updatesError ? (
|
|
||||||
<Alert kind={kinds.WARNING}>
|
|
||||||
{translate('FailedToFetchUpdates')}
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{generalSettingsError ? (
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('FailedToFetchSettings')}
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isMajorUpdateModalOpen}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('InstallMajorVersionUpdate')}
|
|
||||||
message={
|
|
||||||
<div>
|
|
||||||
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown
|
|
||||||
data={translate('InstallMajorVersionUpdateMessageLink', {
|
|
||||||
domain: 'lidarr.audio',
|
|
||||||
url: 'https://lidarr.audio/#downloads',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
confirmLabel={translate('Install')}
|
|
||||||
onConfirm={handleInstallLatestMajorVersionPress}
|
|
||||||
onCancel={handleCancelMajorVersionPress}
|
|
||||||
/>
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Updates;
|
|
98
frontend/src/System/Updates/UpdatesConnector.js
Normal file
98
frontend/src/System/Updates/UpdatesConnector.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
||||||
|
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import Updates from './Updates';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.app.version,
|
||||||
|
createSystemStatusSelector(),
|
||||||
|
(state) => state.system.updates,
|
||||||
|
(state) => state.settings.general,
|
||||||
|
createUISettingsSelector(),
|
||||||
|
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
|
||||||
|
(
|
||||||
|
currentVersion,
|
||||||
|
status,
|
||||||
|
updates,
|
||||||
|
generalSettings,
|
||||||
|
uiSettings,
|
||||||
|
isInstallingUpdate
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
error: updatesError,
|
||||||
|
items
|
||||||
|
} = updates;
|
||||||
|
|
||||||
|
const isFetching = updates.isFetching || generalSettings.isFetching;
|
||||||
|
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentVersion,
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
updatesError,
|
||||||
|
generalSettingsError: generalSettings.error,
|
||||||
|
items,
|
||||||
|
isInstallingUpdate,
|
||||||
|
updateMechanism: generalSettings.item.updateMechanism,
|
||||||
|
updateMechanismMessage: status.packageUpdateMechanismMessage,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchUpdates: fetchUpdates,
|
||||||
|
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||||
|
dispatchExecuteCommand: executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class UpdatesConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchUpdates();
|
||||||
|
this.props.dispatchFetchGeneralSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInstallLatestPress = () => {
|
||||||
|
this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Updates
|
||||||
|
onInstallLatestPress={this.onInstallLatestPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatesConnector.propTypes = {
|
||||||
|
dispatchFetchUpdates: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||||
|
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector);
|
|
@ -6,33 +6,15 @@ import isTomorrow from 'Utilities/Date/isTomorrow';
|
||||||
import isYesterday from 'Utilities/Date/isYesterday';
|
import isYesterday from 'Utilities/Date/isYesterday';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
interface GetRelativeDateOptions {
|
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
|
||||||
timeFormat?: string;
|
|
||||||
includeSeconds?: boolean;
|
|
||||||
timeForToday?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRelativeDate(
|
|
||||||
date: string | undefined,
|
|
||||||
shortDateFormat: string,
|
|
||||||
showRelativeDates: boolean,
|
|
||||||
{
|
|
||||||
timeFormat,
|
|
||||||
includeSeconds = false,
|
|
||||||
timeForToday = false,
|
|
||||||
}: GetRelativeDateOptions = {}
|
|
||||||
) {
|
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return '';
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTodayDate = isToday(date);
|
const isTodayDate = isToday(date);
|
||||||
|
|
||||||
if (isTodayDate && timeForToday && timeFormat) {
|
if (isTodayDate && timeForToday && timeFormat) {
|
||||||
return formatTime(date, timeFormat, {
|
return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
|
||||||
includeMinuteZero: true,
|
|
||||||
includeSeconds,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showRelativeDates) {
|
if (!showRelativeDates) {
|
|
@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise<boolean> {
|
||||||
translations = data.strings;
|
translations = data.strings;
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} catch {
|
} catch (error) {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -27,12 +27,6 @@ export default function translate(
|
||||||
key: string,
|
key: string,
|
||||||
tokens: Record<string, string | number | boolean> = {}
|
tokens: Record<string, string | number | boolean> = {}
|
||||||
) {
|
) {
|
||||||
const { isProduction = true } = window.Lidarr;
|
|
||||||
|
|
||||||
if (!isProduction && !(key in translations)) {
|
|
||||||
console.warn(`Missing translation for key: ${key}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
tokens.appName = 'Lidarr';
|
tokens.appName = 'Lidarr';
|
||||||
|
|
|
@ -131,15 +131,13 @@ class CutoffUnmetConnector extends Component {
|
||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.ALBUM_SEARCH,
|
name: commandNames.ALBUM_SEARCH,
|
||||||
albumIds: selected,
|
albumIds: selected
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllCutoffUnmetPress = () => {
|
onSearchAllCutoffUnmetPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH,
|
name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ function CutoffUnmetRow(props) {
|
||||||
foreignAlbumId,
|
foreignAlbumId,
|
||||||
albumType,
|
albumType,
|
||||||
title,
|
title,
|
||||||
lastSearchTime,
|
|
||||||
disambiguation,
|
disambiguation,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
|
@ -90,15 +89,6 @@ function CutoffUnmetRow(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'albums.lastSearchTime') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={lastSearchTime}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
if (name === 'status') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
@ -142,7 +132,6 @@ CutoffUnmetRow.propTypes = {
|
||||||
foreignAlbumId: PropTypes.string.isRequired,
|
foreignAlbumId: PropTypes.string.isRequired,
|
||||||
albumType: PropTypes.string.isRequired,
|
albumType: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
lastSearchTime: PropTypes.string,
|
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|
|
@ -121,15 +121,13 @@ class MissingConnector extends Component {
|
||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.ALBUM_SEARCH,
|
name: commandNames.ALBUM_SEARCH,
|
||||||
albumIds: selected,
|
albumIds: selected
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllMissingPress = () => {
|
onSearchAllMissingPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.MISSING_ALBUM_SEARCH,
|
name: commandNames.MISSING_ALBUM_SEARCH
|
||||||
commandFinished: this.repopulate
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ function MissingRow(props) {
|
||||||
albumType,
|
albumType,
|
||||||
foreignAlbumId,
|
foreignAlbumId,
|
||||||
title,
|
title,
|
||||||
lastSearchTime,
|
|
||||||
disambiguation,
|
disambiguation,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
|
@ -87,15 +86,6 @@ function MissingRow(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'albums.lastSearchTime') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={lastSearchTime}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<AlbumSearchCellConnector
|
<AlbumSearchCellConnector
|
||||||
|
@ -123,7 +113,6 @@ MissingRow.propTypes = {
|
||||||
foreignAlbumId: PropTypes.string.isRequired,
|
foreignAlbumId: PropTypes.string.isRequired,
|
||||||
albumType: PropTypes.string.isRequired,
|
albumType: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
lastSearchTime: PropTypes.string,
|
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { render } from 'react-dom';
|
||||||
import createAppStore from 'Store/createAppStore';
|
import createAppStore from 'Store/createAppStore';
|
||||||
import App from './App/App';
|
import App from './App/App';
|
||||||
|
|
||||||
|
@ -9,8 +9,9 @@ import 'Diag/ConsoleApi';
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
const store = createAppStore(history);
|
const store = createAppStore(history);
|
||||||
const container = document.getElementById('root');
|
|
||||||
|
|
||||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
render(
|
||||||
root.render(<App store={store} history={history} />);
|
<App store={store} history={history} />,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="/Content/Images/Icons/favicon-16x16.png"
|
href="/Content/Images/Icons/favicon-16x16.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/Content/manifest.json" crossorigin="use-credentials" />
|
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
|
||||||
<link
|
<link
|
||||||
rel="mask-icon"
|
rel="mask-icon"
|
||||||
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="msapplication-config"
|
name="msapplication-config"
|
||||||
content="/Content/browserconfig.xml"
|
content="/Content/Images/Icons/browserconfig.xml"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||||
|
|
|
@ -14,32 +14,6 @@ window.Lidarr = await response.json();
|
||||||
__webpack_public_path__ = `${window.Lidarr.urlBase}/`;
|
__webpack_public_path__ = `${window.Lidarr.urlBase}/`;
|
||||||
/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */
|
/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */
|
||||||
|
|
||||||
const error = console.error;
|
|
||||||
|
|
||||||
// Monkey patch console.error to filter out some warnings from React
|
|
||||||
// TODO: Remove this after the great TypeScript migration
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function logError(...parameters: any[]) {
|
|
||||||
const filter = parameters.find((parameter) => {
|
|
||||||
return (
|
|
||||||
typeof parameter === 'string' &&
|
|
||||||
(parameter.includes(
|
|
||||||
'Support for defaultProps will be removed from function components in a future major release'
|
|
||||||
) ||
|
|
||||||
parameter.includes(
|
|
||||||
'findDOMNode is deprecated and will be removed in the next major release'
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!filter) {
|
|
||||||
error(...parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error = logError;
|
|
||||||
|
|
||||||
const { bootstrap } = await import('./bootstrap');
|
const { bootstrap } = await import('./bootstrap');
|
||||||
|
|
||||||
await bootstrap();
|
await bootstrap();
|
||||||
|
|
|
@ -11,11 +11,8 @@
|
||||||
<!-- Android/Apple Phone -->
|
<!-- Android/Apple Phone -->
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
<meta name="format-detection" content="telephone=no">
|
||||||
content="black-translucent"
|
|
||||||
/>
|
|
||||||
<meta name="format-detection" content="telephone=no" />
|
|
||||||
|
|
||||||
<meta name="description" content="Lidarr" />
|
<meta name="description" content="Lidarr" />
|
||||||
|
|
||||||
|
@ -36,11 +33,7 @@
|
||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="/Content/Images/Icons/favicon-16x16.png"
|
href="/Content/Images/Icons/favicon-16x16.png"
|
||||||
/>
|
/>
|
||||||
<link
|
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
|
||||||
rel="manifest"
|
|
||||||
href="/Content/manifest.json"
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
/>
|
|
||||||
<link
|
<link
|
||||||
rel="mask-icon"
|
rel="mask-icon"
|
||||||
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
||||||
|
@ -52,7 +45,10 @@
|
||||||
href="/favicon.ico"
|
href="/favicon.ico"
|
||||||
data-no-hash
|
data-no-hash
|
||||||
/>
|
/>
|
||||||
<meta name="msapplication-config" content="/Content/browserconfig.xml" />
|
<meta
|
||||||
|
name="msapplication-config"
|
||||||
|
content="/Content/Images/Icons/browserconfig.xml"
|
||||||
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
|
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
|
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
|
||||||
|
@ -63,7 +59,7 @@
|
||||||
body {
|
body {
|
||||||
background-color: var(--pageBackground);
|
background-color: var(--pageBackground);
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
font-family: 'Roboto', 'open sans', 'Helvetica Neue', Helvetica, Arial,
|
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +209,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="sign-in">SIGN IN TO CONTINUE</div>
|
<div class="sign-in">
|
||||||
|
SIGN IN TO CONTINUE
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
role="form"
|
role="form"
|
||||||
|
@ -232,8 +230,8 @@
|
||||||
pattern=".{1,}"
|
pattern=".{1,}"
|
||||||
required
|
required
|
||||||
title="User name is required"
|
title="User name is required"
|
||||||
autofocus="true"
|
autoFocus="true"
|
||||||
autocapitalize="false"
|
autoCapitalize="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -284,16 +282,16 @@
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var yearSpan = document.getElementById('year');
|
var yearSpan = document.getElementById("year");
|
||||||
yearSpan.innerHTML = '2017-' + new Date().getFullYear();
|
yearSpan.innerHTML = "2017-" + new Date().getFullYear();
|
||||||
|
|
||||||
var copyDiv = document.getElementById('copy');
|
var copyDiv = document.getElementById("copy");
|
||||||
copyDiv.classList.remove('hidden');
|
copyDiv.classList.remove("hidden");
|
||||||
|
|
||||||
if (window.location.search.indexOf('loginFailed=true') > -1) {
|
if (window.location.search.indexOf("loginFailed=true") > -1) {
|
||||||
var loginFailedDiv = document.getElementById('login-failed');
|
var loginFailedDiv = document.getElementById("login-failed");
|
||||||
|
|
||||||
loginFailedDiv.classList.remove('hidden');
|
loginFailedDiv.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
var light = {
|
var light = {
|
||||||
|
@ -313,7 +311,7 @@
|
||||||
primaryHoverBorderColor: '#1D563D',
|
primaryHoverBorderColor: '#1D563D',
|
||||||
failedColor: '#f05050',
|
failedColor: '#f05050',
|
||||||
forgotPasswordColor: '#909fa7',
|
forgotPasswordColor: '#909fa7',
|
||||||
forgotPasswordAltColor: '#748690',
|
forgotPasswordAltColor: '#748690'
|
||||||
};
|
};
|
||||||
|
|
||||||
var dark = {
|
var dark = {
|
||||||
|
@ -333,16 +331,21 @@
|
||||||
primaryHoverBorderColor: '#1D563D',
|
primaryHoverBorderColor: '#1D563D',
|
||||||
failedColor: '#f05050',
|
failedColor: '#f05050',
|
||||||
forgotPasswordColor: '#737d83',
|
forgotPasswordColor: '#737d83',
|
||||||
forgotPasswordAltColor: '#546067',
|
forgotPasswordAltColor: '#546067'
|
||||||
};
|
};
|
||||||
|
|
||||||
var theme = '_THEME_';
|
var theme = "_THEME_";
|
||||||
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
var finalTheme =
|
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
|
||||||
theme === 'dark' || (theme === 'auto' && defaultDark) ? dark : light;
|
dark :
|
||||||
|
light;
|
||||||
|
|
||||||
Object.entries(finalTheme).forEach(([key, value]) => {
|
Object.entries(finalTheme).forEach(([key, value]) => {
|
||||||
document.documentElement.style.setProperty(`--${key}`, value);
|
document.documentElement.style.setProperty(
|
||||||
|
`--${key}`,
|
||||||
|
value
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
export type UpdateMechanism =
|
|
||||||
| 'builtIn'
|
|
||||||
| 'script'
|
|
||||||
| 'external'
|
|
||||||
| 'apt'
|
|
||||||
| 'docker';
|
|
||||||
|
|
||||||
export default interface General {
|
|
||||||
bindAddress: string;
|
|
||||||
port: number;
|
|
||||||
sslPort: number;
|
|
||||||
enableSsl: boolean;
|
|
||||||
launchBrowser: boolean;
|
|
||||||
authenticationMethod: string;
|
|
||||||
authenticationRequired: string;
|
|
||||||
analyticsEnabled: boolean;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
passwordConfirmation: string;
|
|
||||||
logLevel: string;
|
|
||||||
consoleLogLevel: string;
|
|
||||||
branch: string;
|
|
||||||
apiKey: string;
|
|
||||||
sslCertPath: string;
|
|
||||||
sslCertPassword: string;
|
|
||||||
urlBase: string;
|
|
||||||
instanceName: string;
|
|
||||||
applicationUrl: string;
|
|
||||||
updateAutomatically: boolean;
|
|
||||||
updateMechanism: UpdateMechanism;
|
|
||||||
updateScriptPath: string;
|
|
||||||
proxyEnabled: boolean;
|
|
||||||
proxyType: string;
|
|
||||||
proxyHostname: string;
|
|
||||||
proxyPort: number;
|
|
||||||
proxyUsername: string;
|
|
||||||
proxyPassword: string;
|
|
||||||
proxyBypassFilter: string;
|
|
||||||
proxyBypassLocalAddresses: boolean;
|
|
||||||
certificateValidation: string;
|
|
||||||
backupFolder: string;
|
|
||||||
backupInterval: number;
|
|
||||||
backupRetention: number;
|
|
||||||
id: number;
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ interface SystemStatus {
|
||||||
osName: string;
|
osName: string;
|
||||||
osVersion: string;
|
osVersion: string;
|
||||||
packageUpdateMechanism: string;
|
packageUpdateMechanism: string;
|
||||||
packageUpdateMechanismMessage: string;
|
|
||||||
runtimeName: string;
|
runtimeName: string;
|
||||||
runtimeVersion: string;
|
runtimeVersion: string;
|
||||||
sqliteVersion: string;
|
sqliteVersion: string;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default interface UiSettings {
|
export interface UiSettings {
|
||||||
theme: 'auto' | 'dark' | 'light';
|
theme: 'auto' | 'dark' | 'light';
|
||||||
showRelativeDates: boolean;
|
showRelativeDates: boolean;
|
||||||
shortDateFormat: string;
|
shortDateFormat: string;
|
|
@ -1,20 +0,0 @@
|
||||||
export interface Changes {
|
|
||||||
new: string[];
|
|
||||||
fixed: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Update {
|
|
||||||
version: string;
|
|
||||||
branch: string;
|
|
||||||
releaseDate: string;
|
|
||||||
fileName: string;
|
|
||||||
url: string;
|
|
||||||
installed: boolean;
|
|
||||||
installedOn: string;
|
|
||||||
installable: boolean;
|
|
||||||
latest: boolean;
|
|
||||||
changes: Changes | null;
|
|
||||||
hash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Update;
|
|
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