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

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
|
@ -9,9 +8,6 @@
|
||||||
|
|
||||||
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> NOTICE - The Lidarr Metadata Server is currently down impacting adding artists, etc. Please follow [GHI 5498](https://github.com/Lidarr/Lidarr/issues/5498) or see Discord for detaila.
|
|
||||||
|
|
||||||
## Major Features Include:
|
## Major Features Include:
|
||||||
|
|
||||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||||
|
|
|
@ -9,18 +9,18 @@ variables:
|
||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '2.13.3'
|
majorVersion: '2.1.6'
|
||||||
minorVersion: $[counter('minorVersion', 1076)]
|
minorVersion: $[counter('minorVersion', 1076)]
|
||||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.427'
|
dotnetVersion: '6.0.417'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '16.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-22.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-11'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
|
@ -166,10 +166,10 @@ stages:
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: UseNode@1
|
- task: NodeTool@0
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
version: $(nodeVersion)
|
versionSpec: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
|
@ -1093,10 +1093,10 @@ stages:
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: UseNode@1
|
- task: NodeTool@0
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
version: $(nodeVersion)
|
versionSpec: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
|
@ -1120,19 +1120,19 @@ stages:
|
||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@1
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'cli'
|
scannerMode: 'CLI'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'lidarr_Lidarr.UI'
|
cliProjectKey: 'lidarr_Lidarr.UI'
|
||||||
cliProjectName: 'LidarrUI'
|
cliProjectName: 'LidarrUI'
|
||||||
cliProjectVersion: '$(lidarrVersion)'
|
cliProjectVersion: '$(lidarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@1
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
|
@ -1208,12 +1208,12 @@ stages:
|
||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@3
|
- task: SonarCloudPrepare@1
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'lidarr'
|
organization: 'lidarr'
|
||||||
scannerMode: 'dotnet'
|
scannerMode: 'MSBuild'
|
||||||
projectKey: 'lidarr_Lidarr'
|
projectKey: 'lidarr_Lidarr'
|
||||||
projectName: 'Lidarr'
|
projectName: 'Lidarr'
|
||||||
projectVersion: '$(lidarrVersion)'
|
projectVersion: '$(lidarrVersion)'
|
||||||
|
@ -1226,16 +1226,21 @@ stages:
|
||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@1
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5.3.11
|
- task: reportgenerator@4
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||||
publishCodeCoverageResults: true
|
- task: PublishCodeCoverageResults@1
|
||||||
|
displayName: Publish Coverage Report
|
||||||
|
inputs:
|
||||||
|
codeCoverageTool: 'cobertura'
|
||||||
|
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||||
|
reportDirectory: './CoverageResults/combined/'
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
|
|
|
@ -1,182 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
### Description: Lidarr .NET Debian install
|
|
||||||
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
|
|
||||||
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
|
|
||||||
### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Lidarr installs
|
|
||||||
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
|
|
||||||
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
|
|
||||||
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
|
|
||||||
|
|
||||||
### Boilerplate Warning
|
|
||||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
scriptversion="1.0.3"
|
|
||||||
scriptdate="2024-01-06"
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
echo "Running Lidarr Install Script - Version [$scriptversion] as of [$scriptdate]"
|
|
||||||
|
|
||||||
# Am I root?, need root!
|
|
||||||
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
|
||||||
echo "Please run as root."
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
app="lidarr"
|
|
||||||
app_port="8686"
|
|
||||||
app_prereq="curl sqlite3 wget"
|
|
||||||
app_umask="0002"
|
|
||||||
branch="main"
|
|
||||||
|
|
||||||
# Constants
|
|
||||||
### Update these variables as required for your specific instance
|
|
||||||
installdir="/opt" # {Update me if needed} Install Location
|
|
||||||
bindir="${installdir}/${app^}" # Full Path to Install Location
|
|
||||||
datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use
|
|
||||||
app_bin=${app^} # Binary Name of the app
|
|
||||||
|
|
||||||
# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir.
|
|
||||||
if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then
|
|
||||||
echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prompt User
|
|
||||||
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
|
|
||||||
app_uid=$(echo "$app_uid" | tr -d ' ')
|
|
||||||
app_uid=${app_uid:-$app}
|
|
||||||
# Prompt Group
|
|
||||||
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
|
|
||||||
app_guid=$(echo "$app_guid" | tr -d ' ')
|
|
||||||
app_guid=${app_guid:-media}
|
|
||||||
|
|
||||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
|
||||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
|
||||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
|
||||||
|
|
||||||
# Create User / Group as needed
|
|
||||||
if [ "$app_guid" != "$app_uid" ]; then
|
|
||||||
if ! getent group "$app_guid" >/dev/null; then
|
|
||||||
groupadd "$app_guid"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if ! getent passwd "$app_uid" >/dev/null; then
|
|
||||||
adduser --system --no-create-home --ingroup "$app_guid" "$app_uid"
|
|
||||||
echo "Created and added User [$app_uid] to Group [$app_guid]"
|
|
||||||
fi
|
|
||||||
if ! getent group "$app_guid" | grep -qw "$app_uid"; then
|
|
||||||
echo "User [$app_uid] did not exist in Group [$app_guid]"
|
|
||||||
usermod -a -G "$app_guid" "$app_uid"
|
|
||||||
echo "Added User [$app_uid] to Group [$app_guid]"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop the App if running
|
|
||||||
if service --status-all | grep -Fq "$app"; then
|
|
||||||
systemctl stop "$app"
|
|
||||||
systemctl disable "$app".service
|
|
||||||
echo "Stopped existing $app"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create Appdata Directory
|
|
||||||
|
|
||||||
# AppData
|
|
||||||
mkdir -p "$datadir"
|
|
||||||
chown -R "$app_uid":"$app_guid" "$datadir"
|
|
||||||
chmod 775 "$datadir"
|
|
||||||
echo "Directories created"
|
|
||||||
# Download and install the App
|
|
||||||
|
|
||||||
# prerequisite packages
|
|
||||||
echo ""
|
|
||||||
echo "Installing pre-requisite Packages"
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
apt update && apt install -y $app_prereq
|
|
||||||
echo ""
|
|
||||||
ARCH=$(dpkg --print-architecture)
|
|
||||||
# get arch
|
|
||||||
dlbase="https://lidarr.servarr.com/v1/update/$branch/updatefile?os=linux&runtime=netcore"
|
|
||||||
case "$ARCH" in
|
|
||||||
"amd64") DLURL="${dlbase}&arch=x64" ;;
|
|
||||||
"armhf") DLURL="${dlbase}&arch=arm" ;;
|
|
||||||
"arm64") DLURL="${dlbase}&arch=arm64" ;;
|
|
||||||
*)
|
|
||||||
echo "Arch not supported"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
echo ""
|
|
||||||
echo "Removing previous tarballs"
|
|
||||||
# -f to Force so we fail if it doesn't exist
|
|
||||||
rm -f "${app^}".*.tar.gz
|
|
||||||
echo ""
|
|
||||||
echo "Downloading..."
|
|
||||||
wget --content-disposition "$DLURL"
|
|
||||||
tar -xvzf "${app^}".*.tar.gz
|
|
||||||
echo ""
|
|
||||||
echo "Installation files downloaded and extracted"
|
|
||||||
|
|
||||||
# remove existing installs
|
|
||||||
echo "Removing existing installation"
|
|
||||||
rm -rf "$bindir"
|
|
||||||
echo "Installing..."
|
|
||||||
mv "${app^}" $installdir
|
|
||||||
chown "$app_uid":"$app_guid" -R "$bindir"
|
|
||||||
chmod 775 "$bindir"
|
|
||||||
rm -rf "${app^}.*.tar.gz"
|
|
||||||
# Ensure we check for an update in case user installs older version or different branch
|
|
||||||
touch "$datadir"/update_required
|
|
||||||
chown "$app_uid":"$app_guid" "$datadir"/update_required
|
|
||||||
echo "App Installed"
|
|
||||||
# Configure Autostart
|
|
||||||
|
|
||||||
# Remove any previous app .service
|
|
||||||
echo "Removing old service file"
|
|
||||||
rm -rf /etc/systemd/system/"$app".service
|
|
||||||
|
|
||||||
# Create app .service with correct user startup
|
|
||||||
echo "Creating service file"
|
|
||||||
cat <<EOF | tee /etc/systemd/system/"$app".service >/dev/null
|
|
||||||
[Unit]
|
|
||||||
Description=${app^} Daemon
|
|
||||||
After=syslog.target network.target
|
|
||||||
[Service]
|
|
||||||
User=$app_uid
|
|
||||||
Group=$app_guid
|
|
||||||
UMask=$app_umask
|
|
||||||
Type=simple
|
|
||||||
ExecStart=$bindir/$app_bin -nobrowser -data=$datadir
|
|
||||||
TimeoutStopSec=20
|
|
||||||
KillMode=process
|
|
||||||
Restart=on-failure
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Start the App
|
|
||||||
echo "Service file created. Attempting to start the app"
|
|
||||||
systemctl -q daemon-reload
|
|
||||||
systemctl enable --now -q "$app"
|
|
||||||
|
|
||||||
# Finish Update/Installation
|
|
||||||
host=$(hostname -I)
|
|
||||||
ip_local=$(grep -oP '^\S*' <<<"$host")
|
|
||||||
echo ""
|
|
||||||
echo "Install complete"
|
|
||||||
sleep 10
|
|
||||||
STATUS="$(systemctl is-active "$app")"
|
|
||||||
if [ "${STATUS}" = "active" ]; then
|
|
||||||
echo "Browse to http://$ip_local:$app_port for the ${app^} GUI"
|
|
||||||
else
|
|
||||||
echo "${app^} failed to start"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Exit
|
|
||||||
exit 0
|
|
|
@ -1,20 +0,0 @@
|
||||||
# This file is owned by the lidarr package, DO NOT MODIFY MANUALLY
|
|
||||||
# Instead use 'dpkg-reconfigure -plow lidarr' to modify User/Group/UMask/-data
|
|
||||||
# Or use systemd built-in override functionality using 'systemctl edit lidarr'
|
|
||||||
[Unit]
|
|
||||||
Description=Lidarr Daemon
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=lidarr
|
|
||||||
Group=lidarr
|
|
||||||
UMask=002
|
|
||||||
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/opt/Lidarr/Lidarr -nobrowser -data=/var/lib/lidarr
|
|
||||||
TimeoutStopSec=20
|
|
||||||
KillMode=process
|
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
23
docs.sh
23
docs.sh
|
@ -1,18 +1,13 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
FRAMEWORK="net6.0"
|
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
ARCHITECTURE="${2:-x64}"
|
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-$ARCHITECTURE"
|
RUNTIME="win-x64"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-$ARCHITECTURE"
|
RUNTIME="linux-x64"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-$ARCHITECTURE"
|
RUNTIME="osx-x64"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -26,21 +21,15 @@ slnFile=src/Lidarr.sln
|
||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
|
||||||
application=Lidarr.Console.dll
|
|
||||||
else
|
|
||||||
application=Lidarr.dll
|
|
||||||
fi
|
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/lidarr.console.dll" v1 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,7 @@ module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
expect: false,
|
expect: false,
|
||||||
chai: false,
|
chai: false,
|
||||||
sinon: false,
|
sinon: false
|
||||||
JSX: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (env) => {
|
||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
target: 'web',
|
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
|
@ -68,7 +67,7 @@ module.exports = (env) => {
|
||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
filename: '[name]-[contenthash].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -93,7 +92,7 @@ module.exports = (env) => {
|
||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
|
@ -135,12 +134,6 @@ module.exports = (env) => {
|
||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, 'Content/robots.txt')
|
destination: path.join(distFolder, 'Content/robots.txt')
|
||||||
},
|
|
||||||
|
|
||||||
// manifest.json and browserconfig.xml
|
|
||||||
{
|
|
||||||
source: 'frontend/src/Content/*.(json|xml)',
|
|
||||||
destination: path.join(distFolder, 'Content')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -188,7 +181,7 @@ module.exports = (env) => {
|
||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: '3.41'
|
corejs: 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -209,7 +202,7 @@ module.exports = (env) => {
|
||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,6 @@ const mixinsFiles = [
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
'autoprefixer',
|
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -172,8 +172,7 @@ function HistoryDetails(props) {
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message,
|
message
|
||||||
indexer
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -193,14 +192,6 @@ function HistoryDetails(props) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
indexer ? (
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
message ?
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|
|
@ -26,5 +26,4 @@
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 90px;
|
width: 90px;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,40 +57,30 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
iconName = icons.PAUSED;
|
iconName = icons.PAUSED;
|
||||||
title = translate('Paused');
|
title = 'Paused';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'queued') {
|
if (status === 'queued') {
|
||||||
iconName = icons.QUEUED;
|
iconName = icons.QUEUED;
|
||||||
title = translate('Queued');
|
title = 'Queued';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = translate('Downloaded');
|
title = 'Downloaded';
|
||||||
|
|
||||||
if (trackedDownloadState === 'importBlocked') {
|
|
||||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importFailed') {
|
|
||||||
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
if (trackedDownloadState === 'importPending') {
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
title += ' - Waiting to Import';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
if (trackedDownloadState === 'importing') {
|
||||||
title += ` - ${translate('Importing')}`;
|
title += ' - Importing';
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
if (trackedDownloadState === 'failedPending') {
|
||||||
title += ` - ${translate('WaitingToProcess')}`;
|
title += ' - Waiting to Process';
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,38 +91,36 @@ function QueueStatusCell(props) {
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
title = translate('Pending');
|
title = 'Pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = translate('PendingDownloadClientUnavailable');
|
title = 'Pending - Download client is unavailable';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'failed') {
|
if (status === 'failed') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage =
|
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||||
errorMessage || translate('CheckDownloadClientForDetails');
|
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOAD;
|
iconName = icons.DOWNLOAD;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('ImportFailed', { sourceTitle });
|
title = `Import failed: ${sourceTitle}`;
|
||||||
} else {
|
} else {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = translate('DownloadFailed');
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,6 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
{
|
{
|
||||||
key: 'blocklistAndSearch',
|
key: 'blocklistAndSearch',
|
||||||
value: translate('BlocklistAndSearch'),
|
value: translate('BlocklistAndSearch'),
|
||||||
isDisabled: isPending,
|
|
||||||
hint: multipleSelected
|
hint: multipleSelected
|
||||||
? translate('BlocklistAndSearchMultipleHint')
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
: translate('BlocklistAndSearchHint'),
|
: translate('BlocklistAndSearchHint'),
|
||||||
|
@ -131,7 +130,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
: translate('BlocklistOnlyHint'),
|
: translate('BlocklistOnlyHint'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [isPending, multipleSelected]);
|
}, [multipleSelected]);
|
||||||
|
|
||||||
const handleRemovalMethodChange = useCallback(
|
const handleRemovalMethodChange = useCallback(
|
||||||
({ value }: { value: RemovalMethod }) => {
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
|
|
@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('AllAlbums')}
|
title={translate('AllAlbums')}
|
||||||
data={translate('MonitorAllAlbums')}
|
data="Monitor all new albums"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('NewAlbums')}
|
title={translate('NewAlbums')}
|
||||||
data={translate('MonitorNewAlbumsData')}
|
data="Monitor new albums released after the newest existing album"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('None')}
|
title={translate('None')}
|
||||||
data={translate('MonitorNoAlbumsData')}
|
data="Don't monitor any new albums"
|
||||||
/>
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,6 @@ export interface Statistics {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Album extends ModelBase {
|
interface Album extends ModelBase {
|
||||||
artistId: number;
|
|
||||||
artist: Artist;
|
artist: Artist;
|
||||||
foreignAlbumId: string;
|
foreignAlbumId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -20,7 +19,6 @@ interface Album extends ModelBase {
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
lastSearchTime?: string;
|
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,10 @@ import Link from 'Components/Link/Link';
|
||||||
|
|
||||||
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
||||||
const link = `/album/${foreignAlbumId}`;
|
const link = `/album/${foreignAlbumId}`;
|
||||||
const albumTitle = `${title}${disambiguation ? ` (${disambiguation})` : ''}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={link} title={albumTitle}>
|
<Link to={link}>
|
||||||
{albumTitle}
|
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
statistics = {},
|
statistics,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|
|
@ -121,8 +121,6 @@
|
||||||
|
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.sizeOnDisk,
|
.sizeOnDisk,
|
||||||
.albumType,
|
|
||||||
.secondaryTypes,
|
|
||||||
.qualityProfileName,
|
.qualityProfileName,
|
||||||
.links,
|
.links,
|
||||||
.tags {
|
.tags {
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'albumNavigationButton': string;
|
'albumNavigationButton': string;
|
||||||
'albumNavigationButtons': string;
|
'albumNavigationButtons': string;
|
||||||
'albumType': string;
|
|
||||||
'alternateTitlesIconContainer': string;
|
'alternateTitlesIconContainer': string;
|
||||||
'backdrop': string;
|
'backdrop': string;
|
||||||
'backdropOverlay': string;
|
'backdropOverlay': string;
|
||||||
|
@ -21,7 +20,6 @@ interface CssExports {
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'qualityProfileName': string;
|
'qualityProfileName': string;
|
||||||
'releaseDate': string;
|
'releaseDate': string;
|
||||||
'secondaryTypes': string;
|
|
||||||
'sizeOnDisk': string;
|
'sizeOnDisk': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
|
|
@ -192,7 +192,6 @@ class AlbumDetails extends Component {
|
||||||
duration,
|
duration,
|
||||||
overview,
|
overview,
|
||||||
albumType,
|
albumType,
|
||||||
secondaryTypes,
|
|
||||||
statistics = {},
|
statistics = {},
|
||||||
monitored,
|
monitored,
|
||||||
releaseDate,
|
releaseDate,
|
||||||
|
@ -205,7 +204,6 @@ class AlbumDetails extends Component {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
tracksError,
|
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
hasTrackFiles,
|
hasTrackFiles,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
|
@ -398,11 +396,10 @@ class AlbumDetails extends Component {
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
duration ?
|
!!duration &&
|
||||||
<span className={styles.duration}>
|
<span className={styles.duration}>
|
||||||
{formatDuration(duration)}
|
{formatDuration(duration)}
|
||||||
</span> :
|
</span>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<HeartRating
|
<HeartRating
|
||||||
|
@ -421,15 +418,14 @@ class AlbumDetails extends Component {
|
||||||
title={translate('ReleaseDate')}
|
title={translate('ReleaseDate')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
<Icon
|
||||||
<Icon
|
name={icons.CALENDAR}
|
||||||
name={icons.CALENDAR}
|
size={17}
|
||||||
size={17}
|
/>
|
||||||
/>
|
|
||||||
<span className={styles.releaseDate}>
|
<span className={styles.releaseDate}>
|
||||||
{moment(releaseDate).format(shortDateFormat)}
|
{moment(releaseDate).format(shortDateFormat)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -438,15 +434,16 @@ class AlbumDetails extends Component {
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div>
|
<Icon
|
||||||
<Icon
|
name={icons.DRIVE}
|
||||||
name={icons.DRIVE}
|
size={17}
|
||||||
size={17}
|
/>
|
||||||
/>
|
|
||||||
<span className={styles.sizeOnDisk}>
|
<span className={styles.sizeOnDisk}>
|
||||||
{formatBytes(sizeOnDisk)}
|
{
|
||||||
</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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,16 @@ import TrackRowConnector from './TrackRowConnector';
|
||||||
import styles from './AlbumDetailsMedium.css';
|
import styles from './AlbumDetailsMedium.css';
|
||||||
|
|
||||||
function getMediumStatistics(tracks) {
|
function getMediumStatistics(tracks) {
|
||||||
const trackCount = tracks.length;
|
let trackCount = 0;
|
||||||
let trackFileCount = 0;
|
let trackFileCount = 0;
|
||||||
let totalTrackCount = 0;
|
let totalTrackCount = 0;
|
||||||
|
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
if (track.trackFileId) {
|
if (track.trackFileId) {
|
||||||
|
trackCount++;
|
||||||
trackFileCount++;
|
trackFileCount++;
|
||||||
|
} else {
|
||||||
|
trackCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTrackCount++;
|
totalTrackCount++;
|
||||||
|
@ -172,7 +175,7 @@ class AlbumDetailsMedium extends Component {
|
||||||
</Table> :
|
</Table> :
|
||||||
|
|
||||||
<div className={styles.noTracks}>
|
<div className={styles.noTracks}>
|
||||||
{translate('NoTracksInThisMedium')}
|
No tracks in this medium
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className={styles.collapseButtonContainer}>
|
<div className={styles.collapseButtonContainer}>
|
||||||
|
|
|
@ -35,9 +35,3 @@
|
||||||
|
|
||||||
width: 55px;
|
width: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indexerFlags {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
|
|
1
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
1
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
|
@ -4,7 +4,6 @@ interface CssExports {
|
||||||
'audio': string;
|
'audio': string;
|
||||||
'customFormatScore': string;
|
'customFormatScore': string;
|
||||||
'duration': string;
|
'duration': string;
|
||||||
'indexerFlags': string;
|
|
||||||
'monitored': string;
|
'monitored': string;
|
||||||
'size': string;
|
'size': string;
|
||||||
'status': string;
|
'status': string;
|
||||||
|
|
|
@ -2,19 +2,15 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AlbumFormats from 'Album/AlbumFormats';
|
import AlbumFormats from 'Album/AlbumFormats';
|
||||||
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
||||||
import IndexerFlags from 'Album/IndexerFlags';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
||||||
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import TrackActionsCell from './TrackActionsCell';
|
import TrackActionsCell from './TrackActionsCell';
|
||||||
import styles from './TrackRow.css';
|
import styles from './TrackRow.css';
|
||||||
|
|
||||||
|
@ -36,7 +32,6 @@ class TrackRow extends Component {
|
||||||
trackFileSize,
|
trackFileSize,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
indexerFlags,
|
|
||||||
columns,
|
columns,
|
||||||
deleteTrackFile
|
deleteTrackFile
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -146,30 +141,12 @@ class TrackRow extends Component {
|
||||||
customFormats.length
|
customFormats.length
|
||||||
)}
|
)}
|
||||||
tooltip={<AlbumFormats formats={customFormats} />}
|
tooltip={<AlbumFormats formats={customFormats} />}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.BOTTOM}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'indexerFlags') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexerFlags}
|
|
||||||
>
|
|
||||||
{indexerFlags ? (
|
|
||||||
<Popover
|
|
||||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
|
||||||
title={translate('IndexerFlags')}
|
|
||||||
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
|
||||||
position={tooltipPositions.LEFT}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'size') {
|
if (name === 'size') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
@ -231,14 +208,12 @@ TrackRow.propTypes = {
|
||||||
trackFileSize: PropTypes.number,
|
trackFileSize: PropTypes.number,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
indexerFlags: PropTypes.number.isRequired,
|
|
||||||
mediaInfo: PropTypes.object,
|
mediaInfo: PropTypes.object,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
TrackRow.defaultProps = {
|
TrackRow.defaultProps = {
|
||||||
customFormats: [],
|
customFormats: []
|
||||||
indexerFlags: 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TrackRow;
|
export default TrackRow;
|
||||||
|
|
|
@ -13,8 +13,7 @@ function createMapStateToProps() {
|
||||||
trackFilePath: trackFile ? trackFile.path : null,
|
trackFilePath: trackFile ? trackFile.path : null,
|
||||||
trackFileSize: trackFile ? trackFile.size : null,
|
trackFileSize: trackFile ? trackFile.size : null,
|
||||||
customFormats: trackFile ? trackFile.customFormats : [],
|
customFormats: trackFile ? trackFile.customFormats : [],
|
||||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0,
|
customFormatScore: trackFile ? trackFile.customFormatScore : 0
|
||||||
indexerFlags: trackFile ? trackFile.indexerFlags : 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
|
||||||
title,
|
title,
|
||||||
artistName,
|
artistName,
|
||||||
albumType,
|
albumType,
|
||||||
statistics = {},
|
statistics,
|
||||||
item,
|
item,
|
||||||
isSaving,
|
isSaving,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
|
||||||
|
|
||||||
interface IndexerFlagsProps {
|
|
||||||
indexerFlags: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
|
||||||
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
|
||||||
|
|
||||||
const flags = allIndexerFlags.items.filter(
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
(item) => (indexerFlags & item.id) === item.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return flags.length ? (
|
|
||||||
<ul>
|
|
||||||
{flags.map((flag, index) => {
|
|
||||||
return <li key={index}>{flag.name}</li>;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IndexerFlags;
|
|
|
@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_EXTRA_LARGE}
|
size={sizes.EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,7 +7,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function AlbumInteractiveSearchModalContent(props) {
|
function AlbumInteractiveSearchModalContent(props) {
|
||||||
const {
|
const {
|
||||||
|
@ -19,10 +18,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{albumTitle === undefined ?
|
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||||
translate('InteractiveSearchModalHeader') :
|
|
||||||
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
|
|
||||||
}
|
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
|
@ -36,7 +32,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
{translate('Close')}
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -12,10 +12,11 @@ function App({ store, history }) {
|
||||||
<DocumentTitle title={window.Lidarr.instanceName}>
|
<DocumentTitle title={window.Lidarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme />
|
<ApplyTheme>
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes app={App} />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
|
</ApplyTheme>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
import NotFound from 'Components/NotFound';
|
import NotFound from 'Components/NotFound';
|
||||||
import Switch from 'Components/Router/Switch';
|
import Switch from 'Components/Router/Switch';
|
||||||
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
||||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import Updates from 'System/Updates/Updates';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
@ -184,7 +184,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/customformats"
|
path="/settings/customformats"
|
||||||
component={CustomFormatSettingsPage}
|
component={CustomFormatSettingsConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={Updates}
|
component={UpdatesConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
50
frontend/src/App/ApplyTheme.js
Normal file
50
frontend/src/App/ApplyTheme.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||||
|
(
|
||||||
|
theme
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
theme
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme({ theme, children }) {
|
||||||
|
// Update the CSS Variables
|
||||||
|
|
||||||
|
const updateCSSVariables = useCallback(() => {
|
||||||
|
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||||
|
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||||
|
|
||||||
|
// Loop through each array key and set the CSS Variables
|
||||||
|
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||||
|
// Based on our snippet from MDN
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
`--${cssVariableKey}`,
|
||||||
|
arrayOfVariableValues[index]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
// On Component Mount and Component Update
|
||||||
|
useEffect(() => {
|
||||||
|
updateCSSVariables(theme);
|
||||||
|
}, [updateCSSVariables, theme]);
|
||||||
|
|
||||||
|
return <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTheme.propTypes = {
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(ApplyTheme);
|
|
@ -1,37 +0,0 @@
|
||||||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import themes from 'Styles/Themes';
|
|
||||||
import AppState from './State/AppState';
|
|
||||||
|
|
||||||
interface ApplyThemeProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createThemeSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme,
|
|
||||||
(theme) => {
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
|
||||||
const theme = useSelector(createThemeSelector());
|
|
||||||
|
|
||||||
const updateCSSVariables = useCallback(() => {
|
|
||||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
|
||||||
document.documentElement.style.setProperty(`--${key}`, value);
|
|
||||||
});
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// On Component Mount and Component Update
|
|
||||||
useEffect(() => {
|
|
||||||
updateCSSVariables();
|
|
||||||
}, [updateCSSVariables, theme]);
|
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ApplyTheme;
|
|
|
@ -1,12 +1,8 @@
|
||||||
import ParseAppState from 'App/State/ParseAppState';
|
|
||||||
import AlbumAppState from './AlbumAppState';
|
import AlbumAppState from './AlbumAppState';
|
||||||
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
|
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
|
||||||
import CommandAppState from './CommandAppState';
|
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
import SystemAppState from './SystemAppState';
|
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
import TrackFilesAppState from './TrackFilesAppState';
|
import TrackFilesAppState from './TrackFilesAppState';
|
||||||
import TracksAppState from './TracksAppState';
|
import TracksAppState from './TracksAppState';
|
||||||
|
@ -44,7 +40,6 @@ export interface CustomFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
version: string;
|
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -57,16 +52,12 @@ interface AppState {
|
||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
artist: ArtistAppState;
|
artist: ArtistAppState;
|
||||||
artistIndex: ArtistIndexAppState;
|
artistIndex: ArtistIndexAppState;
|
||||||
calendar: CalendarAppState;
|
|
||||||
commands: CommandAppState;
|
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
parse: ParseAppState;
|
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
trackFiles: TrackFilesAppState;
|
trackFiles: TrackFilesAppState;
|
||||||
tracksSelection: TracksAppState;
|
tracksSelection: TracksAppState;
|
||||||
system: SystemAppState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppState;
|
export default AppState;
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import Album from 'Album/Album';
|
|
||||||
import AppSectionState, {
|
|
||||||
AppSectionFilterState,
|
|
||||||
} from 'App/State/AppSectionState';
|
|
||||||
|
|
||||||
interface CalendarAppState
|
|
||||||
extends AppSectionState<Album>,
|
|
||||||
AppSectionFilterState<Album> {}
|
|
||||||
|
|
||||||
export default CalendarAppState;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import AppSectionState from 'App/State/AppSectionState';
|
|
||||||
import Command from 'Commands/Command';
|
|
||||||
|
|
||||||
export type CommandAppState = AppSectionState<Command>;
|
|
||||||
|
|
||||||
export default CommandAppState;
|
|
|
@ -1,35 +0,0 @@
|
||||||
import Album from 'Album/Album';
|
|
||||||
import ModelBase from 'App/ModelBase';
|
|
||||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
|
||||||
import Artist from 'Artist/Artist';
|
|
||||||
import { QualityModel } from 'Quality/Quality';
|
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
|
|
||||||
export interface ArtistTitleInfo {
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParsedAlbumInfo {
|
|
||||||
albumTitle: string;
|
|
||||||
artistName: string;
|
|
||||||
artistTitleInfo: ArtistTitleInfo;
|
|
||||||
discography: boolean;
|
|
||||||
quality: QualityModel;
|
|
||||||
releaseGroup?: string;
|
|
||||||
releaseHash: string;
|
|
||||||
releaseTitle: string;
|
|
||||||
releaseTokens: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParseModel extends ModelBase {
|
|
||||||
title: string;
|
|
||||||
parsedAlbumInfo: ParsedAlbumInfo;
|
|
||||||
artist?: Artist;
|
|
||||||
albums: Album[];
|
|
||||||
customFormats?: CustomFormat[];
|
|
||||||
customFormatScore?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParseAppState = AppSectionItemState<ParseModel>;
|
|
||||||
|
|
||||||
export default ParseAppState;
|
|
|
@ -1,28 +1,22 @@
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionItemState,
|
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
import IndexerFlag from 'typings/IndexerFlag';
|
|
||||||
import MetadataProfile from 'typings/MetadataProfile';
|
import MetadataProfile from 'typings/MetadataProfile';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import RootFolder from 'typings/RootFolder';
|
import RootFolder from 'typings/RootFolder';
|
||||||
import General from 'typings/Settings/General';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export type GeneralAppState = AppSectionItemState<General>;
|
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
@ -45,32 +39,22 @@ export interface MetadataProfilesAppState
|
||||||
extends AppSectionState<MetadataProfile>,
|
extends AppSectionState<MetadataProfile>,
|
||||||
AppSectionSchemaState<MetadataProfile> {}
|
AppSectionSchemaState<MetadataProfile> {}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
|
||||||
extends AppSectionState<CustomFormat>,
|
|
||||||
AppSectionDeleteState,
|
|
||||||
AppSectionSaveState {}
|
|
||||||
|
|
||||||
export interface RootFolderAppState
|
export interface RootFolderAppState
|
||||||
extends AppSectionState<RootFolder>,
|
extends AppSectionState<RootFolder>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
|
||||||
customFormats: CustomFormatAppState;
|
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
general: GeneralAppState;
|
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
metadataProfiles: MetadataProfilesAppState;
|
metadataProfiles: MetadataProfilesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
ui: UiSettingsAppState;
|
uiSettings: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsAppState;
|
export default SettingsAppState;
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
|
||||||
import Update from 'typings/Update';
|
|
||||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
|
||||||
|
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
|
||||||
export type UpdateAppState = AppSectionState<Update>;
|
|
||||||
|
|
||||||
interface SystemAppState {
|
|
||||||
updates: UpdateAppState;
|
|
||||||
status: SystemStatusAppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SystemAppState;
|
|
|
@ -1,32 +1,12 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState,
|
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
|
|
||||||
export interface Tag extends ModelBase {
|
export interface Tag extends ModelBase {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagDetail extends ModelBase {
|
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||||
label: string;
|
|
||||||
autoTagIds: number[];
|
|
||||||
delayProfileIds: number[];
|
|
||||||
downloadClientIds: [];
|
|
||||||
importListIds: number[];
|
|
||||||
indexerIds: number[];
|
|
||||||
notificationIds: number[];
|
|
||||||
restrictionIds: number[];
|
|
||||||
artistIds: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TagDetailAppState
|
|
||||||
extends AppSectionState<TagDetail>,
|
|
||||||
AppSectionDeleteState,
|
|
||||||
AppSectionSaveState {}
|
|
||||||
|
|
||||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
|
|
||||||
details: TagDetailAppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TagsAppState;
|
export default TagsAppState;
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface Ratings {
|
||||||
|
|
||||||
interface Artist extends ModelBase {
|
interface Artist extends ModelBase {
|
||||||
added: string;
|
added: string;
|
||||||
|
artistMetadataId: string;
|
||||||
foreignArtistId: string;
|
foreignArtistId: string;
|
||||||
cleanName: string;
|
cleanName: string;
|
||||||
ended: boolean;
|
ended: boolean;
|
||||||
|
@ -35,7 +36,6 @@ interface Artist extends ModelBase {
|
||||||
nextAlbum?: Album;
|
nextAlbum?: Album;
|
||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
metadataProfileId: number;
|
metadataProfileId: number;
|
||||||
monitorNewItems: string;
|
|
||||||
ratings: Ratings;
|
ratings: Ratings;
|
||||||
rootFolderPath: string;
|
rootFolderPath: string;
|
||||||
sortName: string;
|
sortName: string;
|
||||||
|
|
|
@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
{translate('Close')}
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onDeleteArtistConfirmed}
|
onPress={this.onDeleteArtistConfirmed}
|
||||||
>
|
>
|
||||||
{translate('Delete')}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@ -161,7 +161,9 @@ DeleteArtistModalContent.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
DeleteArtistModalContent.defaultProps = {
|
DeleteArtistModalContent.defaultProps = {
|
||||||
statistics: {}
|
statistics: {
|
||||||
|
trackFileCount: 0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteArtistModalContent;
|
export default DeleteArtistModalContent;
|
||||||
|
|
|
@ -10,7 +10,6 @@ function AlbumGroupInfo(props) {
|
||||||
const {
|
const {
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
albumFileCount,
|
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
sizeOnDisk
|
sizeOnDisk
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -31,13 +30,6 @@ function AlbumGroupInfo(props) {
|
||||||
data={monitoredAlbumCount}
|
data={monitoredAlbumCount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
titleClassName={styles.title}
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('WithFiles')}
|
|
||||||
data={albumFileCount}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
titleClassName={styles.title}
|
titleClassName={styles.title}
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
|
@ -58,7 +50,6 @@ function AlbumGroupInfo(props) {
|
||||||
AlbumGroupInfo.propTypes = {
|
AlbumGroupInfo.propTypes = {
|
||||||
totalAlbumCount: PropTypes.number.isRequired,
|
totalAlbumCount: PropTypes.number.isRequired,
|
||||||
monitoredAlbumCount: PropTypes.number.isRequired,
|
monitoredAlbumCount: PropTypes.number.isRequired,
|
||||||
albumFileCount: PropTypes.number.isRequired,
|
|
||||||
trackFileCount: PropTypes.number.isRequired,
|
trackFileCount: PropTypes.number.isRequired,
|
||||||
sizeOnDisk: PropTypes.number.isRequired
|
sizeOnDisk: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
width: 42px;
|
width: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.size,
|
|
||||||
.status {
|
.status {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'monitored': string;
|
'monitored': string;
|
||||||
'size': string;
|
|
||||||
'status': string;
|
'status': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { kinds, sizes } from 'Helpers/Props';
|
import { kinds, sizes } from 'Helpers/Props';
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AlbumRow.css';
|
import styles from './AlbumRow.css';
|
||||||
|
|
||||||
|
@ -88,8 +87,7 @@ class AlbumRow extends Component {
|
||||||
const {
|
const {
|
||||||
trackCount = 0,
|
trackCount = 0,
|
||||||
trackFileCount = 0,
|
trackFileCount = 0,
|
||||||
totalTrackCount = 0,
|
totalTrackCount = 0
|
||||||
sizeOnDisk = 0
|
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -149,7 +147,9 @@ class AlbumRow extends Component {
|
||||||
if (name === 'secondaryTypes') {
|
if (name === 'secondaryTypes') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{secondaryTypes.join(', ')}
|
{
|
||||||
|
secondaryTypes
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ class AlbumRow extends Component {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{
|
{
|
||||||
totalTrackCount
|
statistics.totalTrackCount
|
||||||
}
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
|
@ -196,17 +196,6 @@ class AlbumRow extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'size') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.size}
|
|
||||||
>
|
|
||||||
{!!sizeOnDisk && formatBytes(sizeOnDisk)}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
if (name === 'status') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|
|
@ -192,7 +192,7 @@ class ArtistDetails extends Component {
|
||||||
artistName,
|
artistName,
|
||||||
ratings,
|
ratings,
|
||||||
path,
|
path,
|
||||||
statistics = {},
|
statistics,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
monitored,
|
monitored,
|
||||||
genres,
|
genres,
|
||||||
|
|
|
@ -22,43 +22,32 @@ import styles from './ArtistDetailsSeason.css';
|
||||||
|
|
||||||
function getAlbumStatistics(albums) {
|
function getAlbumStatistics(albums) {
|
||||||
let albumCount = 0;
|
let albumCount = 0;
|
||||||
let albumFileCount = 0;
|
|
||||||
let trackFileCount = 0;
|
let trackFileCount = 0;
|
||||||
let totalAlbumCount = 0;
|
let totalAlbumCount = 0;
|
||||||
let monitoredAlbumCount = 0;
|
let monitoredAlbumCount = 0;
|
||||||
let hasMonitoredAlbums = false;
|
let hasMonitoredAlbums = false;
|
||||||
let sizeOnDisk = 0;
|
let sizeOnDisk = 0;
|
||||||
|
|
||||||
albums.forEach(({ monitored, releaseDate, statistics = {} }) => {
|
albums.forEach((album) => {
|
||||||
const {
|
if (album.statistics) {
|
||||||
trackFileCount: albumTrackFileCount = 0,
|
sizeOnDisk = sizeOnDisk + album.statistics.sizeOnDisk;
|
||||||
totalTrackCount: albumTotalTrackCount = 0,
|
trackFileCount = trackFileCount + album.statistics.trackFileCount;
|
||||||
sizeOnDisk: albumSizeOnDisk = 0
|
|
||||||
} = statistics;
|
|
||||||
|
|
||||||
const hasFiles = albumTrackFileCount > 0 && albumTrackFileCount === albumTotalTrackCount;
|
if (album.statistics.trackFileCount === album.statistics.totalTrackCount || (album.monitored && isBefore(album.airDateUtc))) {
|
||||||
|
albumCount++;
|
||||||
if (hasFiles || (monitored && isBefore(releaseDate))) {
|
}
|
||||||
albumCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFiles) {
|
if (album.monitored) {
|
||||||
albumFileCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitored) {
|
|
||||||
monitoredAlbumCount++;
|
monitoredAlbumCount++;
|
||||||
hasMonitoredAlbums = true;
|
hasMonitoredAlbums = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalAlbumCount++;
|
totalAlbumCount++;
|
||||||
trackFileCount = trackFileCount + albumTrackFileCount;
|
|
||||||
sizeOnDisk = sizeOnDisk + albumSizeOnDisk;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount,
|
albumCount,
|
||||||
albumFileCount,
|
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
|
@ -67,8 +56,8 @@ function getAlbumStatistics(albums) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAlbumCountKind(monitored, albumCount, albumFileCount) {
|
function getAlbumCountKind(monitored, albumCount, monitoredAlbumCount) {
|
||||||
if (albumCount === albumFileCount && albumFileCount > 0) {
|
if (albumCount === monitoredAlbumCount && monitoredAlbumCount > 0) {
|
||||||
return kinds.SUCCESS;
|
return kinds.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,12 +192,11 @@ class ArtistDetailsSeason extends Component {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
albumCount,
|
albumCount,
|
||||||
albumFileCount,
|
|
||||||
totalAlbumCount,
|
totalAlbumCount,
|
||||||
trackFileCount,
|
trackFileCount,
|
||||||
monitoredAlbumCount,
|
monitoredAlbumCount,
|
||||||
hasMonitoredAlbums,
|
hasMonitoredAlbums,
|
||||||
sizeOnDisk = 0
|
sizeOnDisk
|
||||||
} = getAlbumStatistics(items);
|
} = getAlbumStatistics(items);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -238,9 +226,9 @@ class ArtistDetailsSeason extends Component {
|
||||||
anchor={
|
anchor={
|
||||||
<Label
|
<Label
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, albumFileCount)}
|
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, monitoredAlbumCount)}
|
||||||
>
|
>
|
||||||
<span>{albumFileCount} / {albumCount}</span>
|
<span>{albumCount} / {monitoredAlbumCount}</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
title={translate('GroupInformation')}
|
title={translate('GroupInformation')}
|
||||||
|
@ -249,7 +237,6 @@ class ArtistDetailsSeason extends Component {
|
||||||
<AlbumGroupInfo
|
<AlbumGroupInfo
|
||||||
totalAlbumCount={totalAlbumCount}
|
totalAlbumCount={totalAlbumCount}
|
||||||
monitoredAlbumCount={monitoredAlbumCount}
|
monitoredAlbumCount={monitoredAlbumCount}
|
||||||
albumFileCount={albumFileCount}
|
|
||||||
trackFileCount={trackFileCount}
|
trackFileCount={trackFileCount}
|
||||||
sizeOnDisk={sizeOnDisk}
|
sizeOnDisk={sizeOnDisk}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import ArtistTags from './ArtistTags';
|
import ArtistTags from './ArtistTags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -13,8 +12,8 @@ function createMapStateToProps() {
|
||||||
const tags = artist.tags
|
const tags = artist.tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((tag) => !!tag)
|
.filter((tag) => !!tag)
|
||||||
.sort(sortByProp('label'))
|
.map((tag) => tag.label)
|
||||||
.map((tag) => tag.label);
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags
|
tags
|
||||||
|
|
|
@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditArtistModalContent.css';
|
import styles from './EditArtistModalContent.css';
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form {...otherProps}>
|
<Form {...otherProps}>
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Monitored')}
|
{translate('Monitored')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -107,10 +107,9 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MonitorNewItems')}
|
{translate('MonitorNewItems')}
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -133,7 +132,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('QualityProfile')}
|
{translate('QualityProfile')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -147,10 +146,10 @@ class EditArtistModalContent extends Component {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
showMetadataProfile ?
|
showMetadataProfile &&
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MetadataProfile')}
|
Metadata Profile
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
|
@ -174,11 +173,10 @@ class EditArtistModalContent extends Component {
|
||||||
{...metadataProfileId}
|
{...metadataProfileId}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup> :
|
</FormGroup>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Path')}
|
{translate('Path')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -191,7 +189,7 @@ class EditArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Tags')}
|
{translate('Tags')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
@ -211,7 +209,7 @@ class EditArtistModalContent extends Component {
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={onDeleteArtistPress}
|
onPress={onDeleteArtistPress}
|
||||||
>
|
>
|
||||||
{translate('Delete')}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -7,8 +7,6 @@ import React, {
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { SelectProvider } from 'App/SelectContext';
|
import { SelectProvider } from 'App/SelectContext';
|
||||||
import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState';
|
|
||||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
|
||||||
import NoArtist from 'Artist/NoArtist';
|
import NoArtist from 'Artist/NoArtist';
|
||||||
import { RSS_SYNC } from 'Commands/commandNames';
|
import { RSS_SYNC } from 'Commands/commandNames';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
@ -91,19 +89,16 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
view,
|
view,
|
||||||
}: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState =
|
} = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
|
||||||
useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
|
|
||||||
|
|
||||||
const isRssSyncExecuting = useSelector(
|
const isRssSyncExecuting = useSelector(
|
||||||
createCommandExecutingSelector(RSS_SYNC)
|
createCommandExecutingSelector(RSS_SYNC)
|
||||||
);
|
);
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
const scrollerRef = useRef<HTMLDivElement>();
|
||||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
|
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -123,14 +118,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
||||||
}, [isSelectMode, setIsSelectMode]);
|
}, [isSelectMode, setIsSelectMode]);
|
||||||
|
|
||||||
const onTableOptionChange = useCallback(
|
const onTableOptionChange = useCallback(
|
||||||
(payload: unknown) => {
|
(payload) => {
|
||||||
dispatch(setArtistTableOption(payload));
|
dispatch(setArtistTableOption(payload));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onViewSelect = useCallback(
|
const onViewSelect = useCallback(
|
||||||
(value: string) => {
|
(value) => {
|
||||||
dispatch(setArtistView({ view: value }));
|
dispatch(setArtistView({ view: value }));
|
||||||
|
|
||||||
if (scrollerRef.current) {
|
if (scrollerRef.current) {
|
||||||
|
@ -141,14 +136,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSortSelect = useCallback(
|
const onSortSelect = useCallback(
|
||||||
(value: string) => {
|
(value) => {
|
||||||
dispatch(setArtistSort({ sortKey: value }));
|
dispatch(setArtistSort({ sortKey: value }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFilterSelect = useCallback(
|
const onFilterSelect = useCallback(
|
||||||
(value: string) => {
|
(value) => {
|
||||||
dispatch(setArtistFilter({ selectedFilterKey: value }));
|
dispatch(setArtistFilter({ selectedFilterKey: value }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
@ -163,15 +158,15 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
||||||
}, [setIsOptionsModalOpen]);
|
}, [setIsOptionsModalOpen]);
|
||||||
|
|
||||||
const onJumpBarItemPress = useCallback(
|
const onJumpBarItemPress = useCallback(
|
||||||
(character: string) => {
|
(character) => {
|
||||||
setJumpToCharacter(character);
|
setJumpToCharacter(character);
|
||||||
},
|
},
|
||||||
[setJumpToCharacter]
|
[setJumpToCharacter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onScroll = useCallback(
|
const onScroll = useCallback(
|
||||||
({ scrollTop }: { scrollTop: number }) => {
|
({ scrollTop }) => {
|
||||||
setJumpToCharacter(undefined);
|
setJumpToCharacter(null);
|
||||||
scrollPositions.artistIndex = scrollTop;
|
scrollPositions.artistIndex = scrollTop;
|
||||||
},
|
},
|
||||||
[setJumpToCharacter]
|
[setJumpToCharacter]
|
||||||
|
@ -185,10 +180,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const characters = items.reduce((acc: Record<string, number>, item) => {
|
const characters = items.reduce((acc, item) => {
|
||||||
let char = item.sortName.charAt(0);
|
let char = item.sortName.charAt(0);
|
||||||
|
|
||||||
if (!isNaN(Number(char))) {
|
if (!isNaN(char)) {
|
||||||
char = '#';
|
char = '#';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,8 +300,6 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
||||||
<PageContentBody
|
<PageContentBody
|
||||||
ref={scrollerRef}
|
ref={scrollerRef}
|
||||||
className={styles.contentBody}
|
className={styles.contentBody}
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
innerClassName={styles[`${view}InnerContentBody`]}
|
innerClassName={styles[`${view}InnerContentBody`]}
|
||||||
initialScrollTop={props.initialScrollTop}
|
initialScrollTop={props.initialScrollTop}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
|
|
|
@ -23,13 +23,7 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArtistIndexFilterModalProps {
|
export default function ArtistIndexFilterModal(props) {
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ArtistIndexFilterModal(
|
|
||||||
props: ArtistIndexFilterModalProps
|
|
||||||
) {
|
|
||||||
const sectionItems = useSelector(createArtistSelector());
|
const sectionItems = useSelector(createArtistSelector());
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
const customFilterType = 'artist';
|
const customFilterType = 'artist';
|
||||||
|
@ -37,7 +31,7 @@ export default function ArtistIndexFilterModal(
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const dispatchSetFilter = useCallback(
|
const dispatchSetFilter = useCallback(
|
||||||
(payload: unknown) => {
|
(payload) => {
|
||||||
dispatch(setArtistFilter(payload));
|
dispatch(setArtistFilter(payload));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
@ -45,7 +39,6 @@ export default function ArtistIndexFilterModal(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
|
|
@ -206,7 +206,7 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showQualityProfile && !!qualityProfile?.name ? (
|
{showQualityProfile ? (
|
||||||
<div className={styles.title} title={translate('QualityProfile')}>
|
<div className={styles.title} title={translate('QualityProfile')}>
|
||||||
{qualityProfile.name}
|
{qualityProfile.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner';
|
import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner';
|
||||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
@ -22,7 +21,7 @@ const columnPaddingSmallScreen = parseInt(
|
||||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||||
|
|
||||||
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
|
const ADDITIONAL_COLUMN_COUNT = {
|
||||||
small: 3,
|
small: 3,
|
||||||
medium: 2,
|
medium: 2,
|
||||||
large: 1,
|
large: 1,
|
||||||
|
@ -42,17 +41,17 @@ interface CellItemData {
|
||||||
|
|
||||||
interface ArtistIndexBannersProps {
|
interface ArtistIndexBannersProps {
|
||||||
items: Artist[];
|
items: Artist[];
|
||||||
sortKey: string;
|
sortKey?: string;
|
||||||
sortDirection?: SortDirection;
|
sortDirection?: SortDirection;
|
||||||
jumpToCharacter?: string;
|
jumpToCharacter?: string;
|
||||||
scrollTop?: number;
|
scrollTop?: number;
|
||||||
scrollerRef: RefObject<HTMLElement>;
|
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const artistIndexSelector = createSelector(
|
const artistIndexSelector = createSelector(
|
||||||
(state: AppState) => state.artistIndex.bannerOptions,
|
(state) => state.artistIndex.bannerOptions,
|
||||||
(bannerOptions) => {
|
(bannerOptions) => {
|
||||||
return {
|
return {
|
||||||
bannerOptions,
|
bannerOptions,
|
||||||
|
@ -109,7 +108,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { bannerOptions } = useSelector(artistIndexSelector);
|
const { bannerOptions } = useSelector(artistIndexSelector);
|
||||||
const ref = useRef<Grid>(null);
|
const ref: React.MutableRefObject<Grid> = useRef();
|
||||||
const [measureRef, bounds] = useMeasure();
|
const [measureRef, bounds] = useMeasure();
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
@ -223,8 +222,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
||||||
}, [isSmallScreen, scrollerRef, bounds]);
|
}, [isSmallScreen, scrollerRef, bounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
const currentScrollerRef = scrollerRef.current;
|
||||||
|
|
||||||
const handleScroll = throttle(() => {
|
const handleScroll = throttle(() => {
|
||||||
const { offsetTop = 0 } = currentScrollerRef;
|
const { offsetTop = 0 } = currentScrollerRef;
|
||||||
|
@ -233,7 +232,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
||||||
? getWindowScrollTopPosition()
|
? getWindowScrollTopPosition()
|
||||||
: currentScrollerRef.scrollTop) - offsetTop;
|
: currentScrollerRef.scrollTop) - offsetTop;
|
||||||
|
|
||||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||||
|
@ -256,8 +255,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
||||||
|
|
||||||
const scrollTop = rowIndex * rowHeight + padding;
|
const scrollTop = rowIndex * rowHeight + padding;
|
||||||
|
|
||||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
scrollerRef.current.scrollTo(0, scrollTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
|
@ -59,7 +59,7 @@ function ArtistIndexBannerOptionsModalContent(
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onBannerOptionChange = useCallback(
|
const onBannerOptionChange = useCallback(
|
||||||
({ name, value }: { name: string; value: unknown }) => {
|
({ name, value }) => {
|
||||||
dispatch(setArtistBannerOption({ [name]: value }));
|
dispatch(setArtistBannerOption({ [name]: value }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CustomFilter } from 'App/State/AppState';
|
|
||||||
import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal';
|
import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import { align } from 'Helpers/Props';
|
import { align } from 'Helpers/Props';
|
||||||
|
|
||||||
interface ArtistIndexFilterMenuProps {
|
function ArtistIndexFilterMenu(props) {
|
||||||
selectedFilterKey: string | number;
|
|
||||||
filters: object[];
|
|
||||||
customFilters: CustomFilter[];
|
|
||||||
isDisabled: boolean;
|
|
||||||
onFilterSelect(filterName: string): unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) {
|
|
||||||
const {
|
const {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
@ -34,6 +26,15 @@ function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArtistIndexFilterMenu.propTypes = {
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||||
|
.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
ArtistIndexFilterMenu.defaultProps = {
|
ArtistIndexFilterMenu.defaultProps = {
|
||||||
showCustomFilters: false,
|
showCustomFilters: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
import SortMenu from 'Components/Menu/SortMenu';
|
import SortMenu from 'Components/Menu/SortMenu';
|
||||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||||
import { align } from 'Helpers/Props';
|
import { align, sortDirections } from 'Helpers/Props';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
interface SeriesIndexSortMenuProps {
|
function ArtistIndexSortMenu(props) {
|
||||||
sortKey?: string;
|
|
||||||
sortDirection?: SortDirection;
|
|
||||||
isDisabled: boolean;
|
|
||||||
onSortSelect(sortKey: string): unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|
||||||
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
|
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -25,7 +17,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('MonitoredStatus')}
|
Monitored/Status
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -34,7 +26,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Name')}
|
Name
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -43,7 +35,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Type')}
|
Type
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -52,7 +44,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('QualityProfile')}
|
Quality Profile
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -61,7 +53,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('MetadataProfile')}
|
Metadata Profile
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -70,7 +62,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('NextAlbum')}
|
Next Album
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -79,7 +71,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('LastAlbum')}
|
Last Album
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -88,7 +80,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Added')}
|
Added
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -97,7 +89,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Albums')}
|
Albums
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -106,7 +98,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Tracks')}
|
Tracks
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -115,7 +107,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('TrackCount')}
|
Track Count
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -124,7 +116,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Path')}
|
Path
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -133,7 +125,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('SizeOnDisk')}
|
Size on Disk
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
@ -142,11 +134,18 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Tags')}
|
Tags
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
</MenuContent>
|
</MenuContent>
|
||||||
</SortMenu>
|
</SortMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArtistIndexSortMenu.propTypes = {
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default ArtistIndexSortMenu;
|
export default ArtistIndexSortMenu;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
import ViewMenu from 'Components/Menu/ViewMenu';
|
import ViewMenu from 'Components/Menu/ViewMenu';
|
||||||
|
@ -5,13 +6,7 @@ import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
||||||
import { align } from 'Helpers/Props';
|
import { align } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
interface ArtistIndexViewMenuProps {
|
function ArtistIndexViewMenu(props) {
|
||||||
view: string;
|
|
||||||
isDisabled: boolean;
|
|
||||||
onViewSelect(value: string): unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) {
|
|
||||||
const { view, isDisabled, onViewSelect } = props;
|
const { view, isDisabled, onViewSelect } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -41,4 +36,10 @@ function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArtistIndexViewMenu.propTypes = {
|
||||||
|
view: PropTypes.string.isRequired,
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
onViewSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default ArtistIndexViewMenu;
|
export default ArtistIndexViewMenu;
|
||||||
|
|
|
@ -1,51 +1,15 @@
|
||||||
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Album from 'Album/Album';
|
import Album from 'Album/Album';
|
||||||
import { icons } from 'Helpers/Props';
|
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 UiSettings from 'typings/Settings/UiSettings';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
|
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
|
||||||
import styles from './ArtistIndexOverviewInfo.css';
|
import styles from './ArtistIndexOverviewInfo.css';
|
||||||
|
|
||||||
interface RowProps {
|
|
||||||
name: string;
|
|
||||||
showProp: string;
|
|
||||||
valueProp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RowInfoProps {
|
|
||||||
title: string;
|
|
||||||
iconName: IconDefinition;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArtistIndexOverviewInfoProps {
|
|
||||||
height: number;
|
|
||||||
showMonitored: boolean;
|
|
||||||
showQualityProfile: boolean;
|
|
||||||
showLastAlbum: boolean;
|
|
||||||
showAdded: boolean;
|
|
||||||
showAlbumCount: boolean;
|
|
||||||
showPath: boolean;
|
|
||||||
showSizeOnDisk: boolean;
|
|
||||||
monitored: boolean;
|
|
||||||
nextAlbum?: Album;
|
|
||||||
qualityProfile?: QualityProfile;
|
|
||||||
lastAlbum?: Album;
|
|
||||||
added?: string;
|
|
||||||
albumCount: number;
|
|
||||||
path: string;
|
|
||||||
sizeOnDisk?: number;
|
|
||||||
sortKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
|
const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
|
||||||
|
|
||||||
const rows = [
|
const rows = [
|
||||||
|
@ -86,17 +50,11 @@ const rows = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function getInfoRowProps(
|
function getInfoRowProps(row, props, uiSettings) {
|
||||||
row: RowProps,
|
|
||||||
props: ArtistIndexOverviewInfoProps,
|
|
||||||
uiSettings: UiSettings
|
|
||||||
): RowInfoProps | null {
|
|
||||||
const { name } = row;
|
const { name } = row;
|
||||||
|
|
||||||
if (name === 'monitored') {
|
if (name === 'monitored') {
|
||||||
const monitoredText = props.monitored
|
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
||||||
? translate('Monitored')
|
|
||||||
: translate('Unmonitored');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: monitoredText,
|
title: monitoredText,
|
||||||
|
@ -105,9 +63,9 @@ function getInfoRowProps(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
if (name === 'qualityProfileId') {
|
||||||
return {
|
return {
|
||||||
title: translate('QualityProfile'),
|
title: 'Quality Profile',
|
||||||
iconName: icons.PROFILE,
|
iconName: icons.PROFILE,
|
||||||
label: props.qualityProfile.name,
|
label: props.qualityProfile.name,
|
||||||
};
|
};
|
||||||
|
@ -120,16 +78,15 @@ function getInfoRowProps(
|
||||||
return {
|
return {
|
||||||
title: `Last Album: ${lastAlbum.title}`,
|
title: `Last Album: ${lastAlbum.title}`,
|
||||||
iconName: icons.CALENDAR,
|
iconName: icons.CALENDAR,
|
||||||
label:
|
label: getRelativeDate(
|
||||||
getRelativeDate(
|
lastAlbum.releaseDate,
|
||||||
lastAlbum.releaseDate,
|
shortDateFormat,
|
||||||
shortDateFormat,
|
showRelativeDates,
|
||||||
showRelativeDates,
|
{
|
||||||
{
|
timeFormat,
|
||||||
timeFormat,
|
timeForToday: true,
|
||||||
timeForToday: true,
|
}
|
||||||
}
|
),
|
||||||
) ?? '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,11 +98,10 @@ function getInfoRowProps(
|
||||||
return {
|
return {
|
||||||
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
||||||
iconName: icons.ADD,
|
iconName: icons.ADD,
|
||||||
label:
|
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
||||||
getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
timeFormat,
|
||||||
timeFormat,
|
timeForToday: true,
|
||||||
timeForToday: true,
|
}),
|
||||||
}) ?? '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +116,7 @@ function getInfoRowProps(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: translate('AlbumCount'),
|
title: 'Album Count',
|
||||||
iconName: icons.CIRCLE,
|
iconName: icons.CIRCLE,
|
||||||
label: albums,
|
label: albums,
|
||||||
};
|
};
|
||||||
|
@ -168,7 +124,7 @@ function getInfoRowProps(
|
||||||
|
|
||||||
if (name === 'path') {
|
if (name === 'path') {
|
||||||
return {
|
return {
|
||||||
title: translate('Path'),
|
title: 'Path',
|
||||||
iconName: icons.FOLDER,
|
iconName: icons.FOLDER,
|
||||||
label: props.path,
|
label: props.path,
|
||||||
};
|
};
|
||||||
|
@ -176,13 +132,31 @@ function getInfoRowProps(
|
||||||
|
|
||||||
if (name === 'sizeOnDisk') {
|
if (name === 'sizeOnDisk') {
|
||||||
return {
|
return {
|
||||||
title: translate('SizeOnDisk'),
|
title: 'Size on Disk',
|
||||||
iconName: icons.DRIVE,
|
iconName: icons.DRIVE,
|
||||||
label: formatBytes(props.sizeOnDisk),
|
label: formatBytes(props.sizeOnDisk),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
interface ArtistIndexOverviewInfoProps {
|
||||||
|
height: number;
|
||||||
|
showMonitored: boolean;
|
||||||
|
showQualityProfile: boolean;
|
||||||
|
showLastAlbum: boolean;
|
||||||
|
showAdded: boolean;
|
||||||
|
showAlbumCount: boolean;
|
||||||
|
showPath: boolean;
|
||||||
|
showSizeOnDisk: boolean;
|
||||||
|
monitored: boolean;
|
||||||
|
nextAlbum?: Album;
|
||||||
|
qualityProfile: object;
|
||||||
|
lastAlbum?: Album;
|
||||||
|
added?: string;
|
||||||
|
albumCount: number;
|
||||||
|
path: string;
|
||||||
|
sizeOnDisk?: number;
|
||||||
|
sortKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
||||||
|
@ -201,8 +175,6 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
||||||
const { name, showProp, valueProp } = row;
|
const { name, showProp, valueProp } = row;
|
||||||
|
|
||||||
const isVisible =
|
const isVisible =
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore ts(7053)
|
|
||||||
props[valueProp] != null && (props[showProp] || props.sortKey === name);
|
props[valueProp] != null && (props[showProp] || props.sortKey === name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -247,10 +219,6 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
||||||
|
|
||||||
const infoRowProps = getInfoRowProps(row, props, uiSettings);
|
const infoRowProps = getInfoRowProps(row, props, uiSettings);
|
||||||
|
|
||||||
if (infoRowProps == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
|
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import styles from './ArtistIndexOverviewInfoRow.css';
|
import styles from './ArtistIndexOverviewInfoRow.css';
|
||||||
|
|
||||||
interface ArtistIndexOverviewInfoRowProps {
|
interface ArtistIndexOverviewInfoRowProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
iconName?: IconDefinition;
|
iconName: object;
|
||||||
label: string | null;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {
|
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
|
@ -33,11 +33,11 @@ interface RowItemData {
|
||||||
|
|
||||||
interface ArtistIndexOverviewsProps {
|
interface ArtistIndexOverviewsProps {
|
||||||
items: Artist[];
|
items: Artist[];
|
||||||
sortKey: string;
|
sortKey?: string;
|
||||||
sortDirection?: string;
|
sortDirection?: string;
|
||||||
jumpToCharacter?: string;
|
jumpToCharacter?: string;
|
||||||
scrollTop?: number;
|
scrollTop?: number;
|
||||||
scrollerRef: RefObject<HTMLElement>;
|
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
||||||
const { size: posterSize, detailedProgressBar } = useSelector(
|
const { size: posterSize, detailedProgressBar } = useSelector(
|
||||||
selectOverviewOptions
|
selectOverviewOptions
|
||||||
);
|
);
|
||||||
const listRef = useRef<List>(null);
|
const listRef: React.MutableRefObject<List> = useRef();
|
||||||
const [measureRef, bounds] = useMeasure();
|
const [measureRef, bounds] = useMeasure();
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
@ -136,8 +136,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
||||||
}, [isSmallScreen, scrollerRef, bounds]);
|
}, [isSmallScreen, scrollerRef, bounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
const currentScrollerRef = scrollerRef.current;
|
||||||
|
|
||||||
const handleScroll = throttle(() => {
|
const handleScroll = throttle(() => {
|
||||||
const { offsetTop = 0 } = currentScrollerRef;
|
const { offsetTop = 0 } = currentScrollerRef;
|
||||||
|
@ -146,7 +146,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
||||||
? getWindowScrollTopPosition()
|
? getWindowScrollTopPosition()
|
||||||
: currentScrollerRef.scrollTop) - offsetTop;
|
: currentScrollerRef.scrollTop) - offsetTop;
|
||||||
|
|
||||||
listRef.current?.scrollTo(scrollTop);
|
listRef.current.scrollTo(scrollTop);
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||||
|
@ -175,8 +175,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
||||||
scrollTop += offset;
|
scrollTop += offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
listRef.current?.scrollTo(scrollTop);
|
listRef.current.scrollTo(scrollTop);
|
||||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
scrollerRef.current.scrollTo(0, scrollTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||||
|
|
|
@ -60,7 +60,7 @@ function ArtistIndexOverviewOptionsModalContent(
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onOverviewOptionChange = useCallback(
|
const onOverviewOptionChange = useCallback(
|
||||||
({ name, value }: { name: string; value: unknown }) => {
|
({ name, value }) => {
|
||||||
dispatch(setArtistOverviewOption({ [name]: value }));
|
dispatch(setArtistOverviewOption({ [name]: value }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|
|
@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showQualityProfile && !!qualityProfile?.name ? (
|
{showQualityProfile ? (
|
||||||
<div className={styles.title} title={translate('QualityProfile')}>
|
<div className={styles.title} title={translate('QualityProfile')}>
|
||||||
{qualityProfile.name}
|
{qualityProfile.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster';
|
import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster';
|
||||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
@ -22,7 +21,7 @@ const columnPaddingSmallScreen = parseInt(
|
||||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||||
|
|
||||||
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
|
const ADDITIONAL_COLUMN_COUNT = {
|
||||||
small: 3,
|
small: 3,
|
||||||
medium: 2,
|
medium: 2,
|
||||||
large: 1,
|
large: 1,
|
||||||
|
@ -42,17 +41,17 @@ interface CellItemData {
|
||||||
|
|
||||||
interface ArtistIndexPostersProps {
|
interface ArtistIndexPostersProps {
|
||||||
items: Artist[];
|
items: Artist[];
|
||||||
sortKey: string;
|
sortKey?: string;
|
||||||
sortDirection?: SortDirection;
|
sortDirection?: SortDirection;
|
||||||
jumpToCharacter?: string;
|
jumpToCharacter?: string;
|
||||||
scrollTop?: number;
|
scrollTop?: number;
|
||||||
scrollerRef: RefObject<HTMLElement>;
|
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const artistIndexSelector = createSelector(
|
const artistIndexSelector = createSelector(
|
||||||
(state: AppState) => state.artistIndex.posterOptions,
|
(state) => state.artistIndex.posterOptions,
|
||||||
(posterOptions) => {
|
(posterOptions) => {
|
||||||
return {
|
return {
|
||||||
posterOptions,
|
posterOptions,
|
||||||
|
@ -109,7 +108,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { posterOptions } = useSelector(artistIndexSelector);
|
const { posterOptions } = useSelector(artistIndexSelector);
|
||||||
const ref = useRef<Grid>(null);
|
const ref: React.MutableRefObject<Grid> = useRef();
|
||||||
const [measureRef, bounds] = useMeasure();
|
const [measureRef, bounds] = useMeasure();
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
@ -232,8 +231,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
||||||
}, [isSmallScreen, size, scrollerRef, bounds]);
|
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
const currentScrollerRef = scrollerRef.current;
|
||||||
|
|
||||||
const handleScroll = throttle(() => {
|
const handleScroll = throttle(() => {
|
||||||
const { offsetTop = 0 } = currentScrollerRef;
|
const { offsetTop = 0 } = currentScrollerRef;
|
||||||
|
@ -242,7 +241,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
||||||
? getWindowScrollTopPosition()
|
? getWindowScrollTopPosition()
|
||||||
: currentScrollerRef.scrollTop) - offsetTop;
|
: currentScrollerRef.scrollTop) - offsetTop;
|
||||||
|
|
||||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||||
|
@ -265,8 +264,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
||||||
|
|
||||||
const scrollTop = rowIndex * rowHeight + padding;
|
const scrollTop = rowIndex * rowHeight + padding;
|
||||||
|
|
||||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
scrollerRef.current.scrollTo(0, scrollTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
|
@ -59,7 +59,7 @@ function ArtistIndexPosterOptionsModalContent(
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onPosterOptionChange = useCallback(
|
const onPosterOptionChange = useCallback(
|
||||||
({ name, value }: { name: string; value: unknown }) => {
|
({ name, value }) => {
|
||||||
dispatch(setArtistPosterOption({ [name]: value }));
|
dispatch(setArtistPosterOption({ [name]: value }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Statistics } from 'Album/Album';
|
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
@ -57,8 +56,8 @@ function AlbumDetails(props: AlbumDetailsProps) {
|
||||||
disambiguation,
|
disambiguation,
|
||||||
albumType,
|
albumType,
|
||||||
monitored,
|
monitored,
|
||||||
statistics = {} as Statistics,
|
statistics,
|
||||||
isSaving = false,
|
isSaving,
|
||||||
} = album;
|
} = album;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps {
|
||||||
artistId: number;
|
artistId: number;
|
||||||
albumId: number;
|
albumId: number;
|
||||||
title: string;
|
title: string;
|
||||||
disambiguation?: string;
|
disambiguation: string;
|
||||||
albumType: string;
|
albumType: string;
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
|
|
|
@ -33,7 +33,7 @@ function ChangeMonitoringModalContent(
|
||||||
const [monitor, setMonitor] = useState(NO_CHANGE);
|
const [monitor, setMonitor] = useState(NO_CHANGE);
|
||||||
|
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
({ value }: { value: string }) => {
|
({ value }) => {
|
||||||
setMonitor(value);
|
setMonitor(value);
|
||||||
},
|
},
|
||||||
[setMonitor]
|
[setMonitor]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { SyntheticEvent, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
@ -15,9 +15,8 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
|
||||||
const isSelected = selectState.selectedState[artistId];
|
const isSelected = selectState.selectedState[artistId];
|
||||||
|
|
||||||
const onSelectPress = useCallback(
|
const onSelectPress = useCallback(
|
||||||
(event: SyntheticEvent) => {
|
(event) => {
|
||||||
const nativeEvent = event.nativeEvent as PointerEvent;
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
const shiftKey = nativeEvent.shiftKey;
|
|
||||||
|
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: 'toggleSelected',
|
type: 'toggleSelected',
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
||||||
interface ArtistIndexSelectAllButtonProps {
|
interface ArtistIndexSelectAllButtonProps {
|
||||||
label: string;
|
label: string;
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
overflowComponent: React.FunctionComponent<never>;
|
overflowComponent: React.FunctionComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {
|
function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {
|
||||||
|
|
|
@ -24,14 +24,6 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal';
|
||||||
import TagsModal from './Tags/TagsModal';
|
import TagsModal from './Tags/TagsModal';
|
||||||
import styles from './ArtistIndexSelectFooter.css';
|
import styles from './ArtistIndexSelectFooter.css';
|
||||||
|
|
||||||
interface SavePayload {
|
|
||||||
monitored?: boolean;
|
|
||||||
qualityProfileId?: number;
|
|
||||||
metadataProfileId?: number;
|
|
||||||
rootFolderPath?: string;
|
|
||||||
moveFiles?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const artistEditorSelector = createSelector(
|
const artistEditorSelector = createSelector(
|
||||||
(state: AppState) => state.artist,
|
(state: AppState) => state.artist,
|
||||||
(artist) => {
|
(artist) => {
|
||||||
|
@ -87,7 +79,7 @@ function ArtistIndexSelectFooter() {
|
||||||
}, [setIsEditModalOpen]);
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
const onSavePress = useCallback(
|
const onSavePress = useCallback(
|
||||||
(payload: SavePayload) => {
|
(payload) => {
|
||||||
setIsSavingArtist(true);
|
setIsSavingArtist(true);
|
||||||
setIsEditModalOpen(false);
|
setIsEditModalOpen(false);
|
||||||
|
|
||||||
|
@ -126,7 +118,7 @@ function ArtistIndexSelectFooter() {
|
||||||
}, [setIsTagsModalOpen]);
|
}, [setIsTagsModalOpen]);
|
||||||
|
|
||||||
const onApplyTagsPress = useCallback(
|
const onApplyTagsPress = useCallback(
|
||||||
(tags: number[], applyTags: string) => {
|
(tags, applyTags) => {
|
||||||
setIsSavingTags(true);
|
setIsSavingTags(true);
|
||||||
setIsTagsModalOpen(false);
|
setIsTagsModalOpen(false);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps {
|
||||||
label: string;
|
label: string;
|
||||||
iconName: IconDefinition;
|
iconName: IconDefinition;
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
overflowComponent: React.FunctionComponent<never>;
|
overflowComponent: React.FunctionComponent;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,15 +28,9 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const artistNames = useMemo(() => {
|
const artistNames = useMemo(() => {
|
||||||
const artists = artistIds.reduce((acc: Artist[], id) => {
|
const artists = artistIds.map((id) => {
|
||||||
const a = allArtists.find((a) => a.id === id);
|
return allArtists.find((a) => a.id === id);
|
||||||
|
});
|
||||||
if (a) {
|
|
||||||
acc.push(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sorted = orderBy(artists, ['sortName']);
|
const sorted = orderBy(artists, ['sortName']);
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions';
|
import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions';
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './DeleteArtistModalContent.css';
|
import styles from './DeleteArtistModalContent.css';
|
||||||
|
|
||||||
|
@ -38,16 +37,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) {
|
||||||
|
|
||||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
const artists = useMemo((): Artist[] => {
|
const artists = useMemo(() => {
|
||||||
const artistList = artistIds.map((id) => {
|
const artists = artistIds.map((id) => {
|
||||||
return allArtists.find((a) => a.id === id);
|
return allArtists.find((a) => a.id === id);
|
||||||
}) as Artist[];
|
});
|
||||||
|
|
||||||
return orderBy(artistList, ['sortName']);
|
return orderBy(artists, ['sortName']);
|
||||||
}, [artistIds, allArtists]);
|
}, [artistIds, allArtists]);
|
||||||
|
|
||||||
const onDeleteFilesChange = useCallback(
|
const onDeleteFilesChange = useCallback(
|
||||||
({ value }: CheckInputChanged) => {
|
({ value }) => {
|
||||||
setDeleteFiles(value);
|
setDeleteFiles(value);
|
||||||
},
|
},
|
||||||
[setDeleteFiles]
|
[setDeleteFiles]
|
||||||
|
|
|
@ -35,7 +35,7 @@ const monitoredOptions = [
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
isDisabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'monitored',
|
key: 'monitored',
|
||||||
|
@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
|
||||||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||||
|
|
||||||
const save = useCallback(
|
const save = useCallback(
|
||||||
(moveFiles: boolean) => {
|
(moveFiles) => {
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
const payload: SavePayload = {};
|
const payload: SavePayload = {};
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
({ name, value }: { name: string; value: string }) => {
|
({ name, value }) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'monitored':
|
case 'monitored':
|
||||||
setMonitored(value);
|
setMonitored(value);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Tag } from 'App/State/TagsAppState';
|
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
@ -29,7 +28,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
const { artistIds, onModalClose, onApplyTagsPress } = props;
|
const { artistIds, onModalClose, onApplyTagsPress } = props;
|
||||||
|
|
||||||
const allArtists: Artist[] = useSelector(createAllArtistSelector());
|
const allArtists: Artist[] = useSelector(createAllArtistSelector());
|
||||||
const tagList: Tag[] = useSelector(createTagsSelector());
|
const tagList = useSelector(createTagsSelector());
|
||||||
|
|
||||||
const [tags, setTags] = useState<number[]>([]);
|
const [tags, setTags] = useState<number[]>([]);
|
||||||
const [applyTags, setApplyTags] = useState('add');
|
const [applyTags, setApplyTags] = useState('add');
|
||||||
|
@ -49,14 +48,14 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
}, [artistIds, allArtists]);
|
}, [artistIds, allArtists]);
|
||||||
|
|
||||||
const onTagsChange = useCallback(
|
const onTagsChange = useCallback(
|
||||||
({ value }: { value: number[] }) => {
|
({ value }) => {
|
||||||
setTags(value);
|
setTags(value);
|
||||||
},
|
},
|
||||||
[setTags]
|
[setTags]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onApplyTagsChange = useCallback(
|
const onApplyTagsChange = useCallback(
|
||||||
({ value }: { value: string }) => {
|
({ value }) => {
|
||||||
setApplyTags(value);
|
setApplyTags(value);
|
||||||
},
|
},
|
||||||
[setApplyTags]
|
[setApplyTags]
|
||||||
|
|
|
@ -67,7 +67,6 @@
|
||||||
flex: 1 0 125px;
|
flex: 1 0 125px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monitorNewItems,
|
|
||||||
.nextAlbum,
|
.nextAlbum,
|
||||||
.lastAlbum,
|
.lastAlbum,
|
||||||
.added,
|
.added,
|
||||||
|
|
|
@ -14,7 +14,6 @@ interface CssExports {
|
||||||
'lastAlbum': string;
|
'lastAlbum': string;
|
||||||
'link': string;
|
'link': string;
|
||||||
'metadataProfileId': string;
|
'metadataProfileId': string;
|
||||||
'monitorNewItems': string;
|
|
||||||
'nextAlbum': string;
|
'nextAlbum': string;
|
||||||
'overlayTitle': string;
|
'overlayTitle': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
|
|
|
@ -23,9 +23,7 @@ import Column from 'Components/Table/Column';
|
||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AlbumsCell from './AlbumsCell';
|
import AlbumsCell from './AlbumsCell';
|
||||||
import hasGrowableColumns from './hasGrowableColumns';
|
import hasGrowableColumns from './hasGrowableColumns';
|
||||||
|
@ -58,7 +56,6 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
monitored,
|
monitored,
|
||||||
status,
|
status,
|
||||||
path,
|
path,
|
||||||
monitorNewItems,
|
|
||||||
nextAlbum,
|
nextAlbum,
|
||||||
lastAlbum,
|
lastAlbum,
|
||||||
added,
|
added,
|
||||||
|
@ -129,7 +126,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
}, [setIsDeleteArtistModalOpen]);
|
}, [setIsDeleteArtistModalOpen]);
|
||||||
|
|
||||||
const onSelectedChange = useCallback(
|
const onSelectedChange = useCallback(
|
||||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
({ id, value, shiftKey }) => {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: 'toggleSelected',
|
type: 'toggleSelected',
|
||||||
id,
|
id,
|
||||||
|
@ -220,7 +217,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
if (name === 'qualityProfileId') {
|
if (name === 'qualityProfileId') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
{qualityProfile?.name ?? ''}
|
{qualityProfile.name}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'qualityProfileId') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{qualityProfile.name}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -228,15 +233,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
if (name === 'metadataProfileId') {
|
if (name === 'metadataProfileId') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
{metadataProfile?.name ?? ''}
|
{metadataProfile.name}
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'monitorNewItems') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
|
||||||
{translate(firstCharToUpper(monitorNewItems))}
|
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -255,7 +252,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
{translate('None')}
|
None
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -274,15 +271,13 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
{translate('None')}
|
None
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'added') {
|
if (name === 'added') {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore ts(2739)
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
|
@ -333,7 +328,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||||
if (name === 'path') {
|
if (name === 'path') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
<span title={path}>{path}</span>
|
{path}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
|
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
|
||||||
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
|
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
|
||||||
|
@ -31,17 +30,17 @@ interface RowItemData {
|
||||||
|
|
||||||
interface ArtistIndexTableProps {
|
interface ArtistIndexTableProps {
|
||||||
items: Artist[];
|
items: Artist[];
|
||||||
sortKey: string;
|
sortKey?: string;
|
||||||
sortDirection?: SortDirection;
|
sortDirection?: SortDirection;
|
||||||
jumpToCharacter?: string;
|
jumpToCharacter?: string;
|
||||||
scrollTop?: number;
|
scrollTop?: number;
|
||||||
scrollerRef: RefObject<HTMLElement>;
|
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnsSelector = createSelector(
|
const columnsSelector = createSelector(
|
||||||
(state: AppState) => state.artistIndex.columns,
|
(state) => state.artistIndex.columns,
|
||||||
(columns) => columns
|
(columns) => columns
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -94,7 +93,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
||||||
|
|
||||||
const columns = useSelector(columnsSelector);
|
const columns = useSelector(columnsSelector);
|
||||||
const { showBanners } = useSelector(selectTableOptions);
|
const { showBanners } = useSelector(selectTableOptions);
|
||||||
const listRef = useRef<List<RowItemData>>(null);
|
const listRef: React.MutableRefObject<List> = useRef();
|
||||||
const [measureRef, bounds] = useMeasure();
|
const [measureRef, bounds] = useMeasure();
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
const windowWidth = window.innerWidth;
|
const windowWidth = window.innerWidth;
|
||||||
|
@ -105,7 +104,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
||||||
}, [showBanners]);
|
}, [showBanners]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const current = scrollerRef?.current as HTMLElement;
|
const current = scrollerRef.current as HTMLElement;
|
||||||
|
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
setSize({
|
setSize({
|
||||||
|
@ -129,8 +128,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
||||||
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
|
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
const currentScrollerRef = scrollerRef.current;
|
||||||
|
|
||||||
const handleScroll = throttle(() => {
|
const handleScroll = throttle(() => {
|
||||||
const { offsetTop = 0 } = currentScrollerRef;
|
const { offsetTop = 0 } = currentScrollerRef;
|
||||||
|
@ -139,7 +138,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
||||||
? getWindowScrollTopPosition()
|
? getWindowScrollTopPosition()
|
||||||
: currentScrollerRef.scrollTop) - offsetTop;
|
: currentScrollerRef.scrollTop) - offsetTop;
|
||||||
|
|
||||||
listRef.current?.scrollTo(scrollTop);
|
listRef.current.scrollTo(scrollTop);
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||||
|
@ -168,8 +167,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
||||||
scrollTop += offset;
|
scrollTop += offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
listRef.current?.scrollTo(scrollTop);
|
listRef.current.scrollTo(scrollTop);
|
||||||
scrollerRef?.current?.scrollTo(0, scrollTop);
|
scrollerRef.current.scrollTo(0, scrollTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||||
|
|
|
@ -31,7 +31,6 @@
|
||||||
flex: 1 0 125px;
|
flex: 1 0 125px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monitorNewItems,
|
|
||||||
.nextAlbum,
|
.nextAlbum,
|
||||||
.lastAlbum,
|
.lastAlbum,
|
||||||
.added,
|
.added,
|
||||||
|
|
|
@ -11,7 +11,6 @@ interface CssExports {
|
||||||
'lastAlbum': string;
|
'lastAlbum': string;
|
||||||
'latestAlbum': string;
|
'latestAlbum': string;
|
||||||
'metadataProfileId': string;
|
'metadataProfileId': string;
|
||||||
'monitorNewItems': string;
|
|
||||||
'nextAlbum': string;
|
'nextAlbum': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
'qualityProfileId': string;
|
'qualityProfileId': string;
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
setArtistSort,
|
setArtistSort,
|
||||||
setArtistTableOption,
|
setArtistTableOption,
|
||||||
} from 'Store/Actions/artistIndexActions';
|
} from 'Store/Actions/artistIndexActions';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
|
||||||
import hasGrowableColumns from './hasGrowableColumns';
|
import hasGrowableColumns from './hasGrowableColumns';
|
||||||
import styles from './ArtistIndexTableHeader.css';
|
import styles from './ArtistIndexTableHeader.css';
|
||||||
|
|
||||||
|
@ -33,21 +32,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
||||||
const [selectState, selectDispatch] = useSelect();
|
const [selectState, selectDispatch] = useSelect();
|
||||||
|
|
||||||
const onSortPress = useCallback(
|
const onSortPress = useCallback(
|
||||||
(value: string) => {
|
(value) => {
|
||||||
dispatch(setArtistSort({ sortKey: value }));
|
dispatch(setArtistSort({ sortKey: value }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTableOptionChange = useCallback(
|
const onTableOptionChange = useCallback(
|
||||||
(payload: unknown) => {
|
(payload) => {
|
||||||
dispatch(setArtistTableOption(payload));
|
dispatch(setArtistTableOption(payload));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }: CheckInputChanged) => {
|
({ value }) => {
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
type: value ? 'selectAll' : 'unselectAll',
|
type: value ? 'selectAll' : 'unselectAll',
|
||||||
});
|
});
|
||||||
|
@ -95,8 +94,6 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
key={name}
|
key={name}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
styles[name],
|
styles[name],
|
||||||
name === 'sortName' && showBanners && styles.banner,
|
name === 'sortName' && showBanners && styles.banner,
|
||||||
name === 'sortName' &&
|
name === 'sortName' &&
|
||||||
|
|
|
@ -4,7 +4,6 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import selectTableOptions from './selectTableOptions';
|
import selectTableOptions from './selectTableOptions';
|
||||||
|
|
||||||
|
@ -20,7 +19,7 @@ function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) {
|
||||||
const { showBanners, showSearchAction } = tableOptions;
|
const { showBanners, showSearchAction } = tableOptions;
|
||||||
|
|
||||||
const onTableOptionChangeWrapper = useCallback(
|
const onTableOptionChangeWrapper = useCallback(
|
||||||
({ name, value }: CheckInputChanged) => {
|
({ name, value }) => {
|
||||||
onTableOptionChange({
|
onTableOptionChange({
|
||||||
tableOptions: {
|
tableOptions: {
|
||||||
...tableOptions,
|
...tableOptions,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import Artist from 'Artist/Artist';
|
import Artist from 'Artist/Artist';
|
||||||
import Command from 'Commands/Command';
|
|
||||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||||
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
||||||
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
||||||
|
@ -13,21 +12,25 @@ function createArtistIndexItemSelector(artistId: number) {
|
||||||
createArtistQualityProfileSelector(artistId),
|
createArtistQualityProfileSelector(artistId),
|
||||||
createArtistMetadataProfileSelector(artistId),
|
createArtistMetadataProfileSelector(artistId),
|
||||||
createExecutingCommandsSelector(),
|
createExecutingCommandsSelector(),
|
||||||
(
|
(artist: Artist, qualityProfile, metadataProfile, executingCommands) => {
|
||||||
artist: Artist,
|
// If an artist is deleted this selector may fire before the parent
|
||||||
qualityProfile,
|
// selectors, which will result in an undefined artist, if that happens
|
||||||
metadataProfile,
|
// we want to return early here and again in the render function to avoid
|
||||||
executingCommands: Command[]
|
// trying to show an artist that has no information available.
|
||||||
) => {
|
|
||||||
|
if (!artist) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const isRefreshingArtist = executingCommands.some((command) => {
|
const isRefreshingArtist = executingCommands.some((command) => {
|
||||||
return (
|
return (
|
||||||
command.name === REFRESH_ARTIST && command.body.artistId === artistId
|
command.name === REFRESH_ARTIST && command.body.artistId === artist.id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSearchingArtist = executingCommands.some((command) => {
|
const isSearchingArtist = executingCommands.some((command) => {
|
||||||
return (
|
return (
|
||||||
command.name === ARTIST_SEARCH && command.body.artistId === artistId
|
command.name === ARTIST_SEARCH && command.body.artistId === artist.id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_EXTRA_LARGE}
|
size={sizes.EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
|
||||||
import { setCalendarFilter } from 'Store/Actions/calendarActions';
|
|
||||||
|
|
||||||
function createCalendarSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.calendar.items,
|
|
||||||
(calendar) => {
|
|
||||||
return calendar;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFilterBuilderPropsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.calendar.filterBuilderProps,
|
|
||||||
(filterBuilderProps) => {
|
|
||||||
return filterBuilderProps;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CalendarFilterModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
|
||||||
const sectionItems = useSelector(createCalendarSelector());
|
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
|
||||||
const customFilterType = 'calendar';
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const dispatchSetFilter = useCallback(
|
|
||||||
(payload: unknown) => {
|
|
||||||
dispatch(setCalendarFilter(payload));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterModal
|
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
|
||||||
sectionItems={sectionItems}
|
|
||||||
filterBuilderProps={filterBuilderProps}
|
|
||||||
customFilterType={customFilterType}
|
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -14,7 +14,6 @@ import { align, icons } from 'Helpers/Props';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CalendarConnector from './CalendarConnector';
|
import CalendarConnector from './CalendarConnector';
|
||||||
import CalendarFilterModal from './CalendarFilterModal';
|
|
||||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||||
import LegendConnector from './Legend/LegendConnector';
|
import LegendConnector from './Legend/LegendConnector';
|
||||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||||
|
@ -79,7 +78,6 @@ class CalendarPage extends Component {
|
||||||
const {
|
const {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
customFilters,
|
|
||||||
hasArtist,
|
hasArtist,
|
||||||
artistError,
|
artistError,
|
||||||
artistIsFetching,
|
artistIsFetching,
|
||||||
|
@ -139,8 +137,7 @@ class CalendarPage extends Component {
|
||||||
isDisabled={!hasArtist}
|
isDisabled={!hasArtist}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={customFilters}
|
customFilters={[]}
|
||||||
filterModalConnectorComponent={CalendarFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
@ -207,7 +204,6 @@ class CalendarPage extends Component {
|
||||||
CalendarPage.propTypes = {
|
CalendarPage.propTypes = {
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hasArtist: PropTypes.bool.isRequired,
|
hasArtist: PropTypes.bool.isRequired,
|
||||||
artistError: PropTypes.object,
|
artistError: PropTypes.object,
|
||||||
artistIsFetching: PropTypes.bool.isRequired,
|
artistIsFetching: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
@ -60,7 +59,6 @@ function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.calendar.selectedFilterKey,
|
(state) => state.calendar.selectedFilterKey,
|
||||||
(state) => state.calendar.filters,
|
(state) => state.calendar.filters,
|
||||||
createCustomFiltersSelector('calendar'),
|
|
||||||
createArtistCountSelector(),
|
createArtistCountSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
createMissingAlbumIdsSelector(),
|
createMissingAlbumIdsSelector(),
|
||||||
|
@ -69,7 +67,6 @@ function createMapStateToProps() {
|
||||||
(
|
(
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
customFilters,
|
|
||||||
artistCount,
|
artistCount,
|
||||||
uiSettings,
|
uiSettings,
|
||||||
missingAlbumIds,
|
missingAlbumIds,
|
||||||
|
@ -79,7 +76,6 @@ function createMapStateToProps() {
|
||||||
return {
|
return {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
customFilters,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
hasArtist: !!artistCount.count,
|
hasArtist: !!artistCount.count,
|
||||||
artistError: artistCount.error,
|
artistError: artistCount.error,
|
||||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
helpText={translate('ICalTagsArtistHelpText')}
|
helpText={translate('TagsHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
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