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
|
||||
- Develop
|
||||
- Nightly
|
||||
- Plugins (experimental)
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v4
|
||||
- uses: dessant/label-actions@v3
|
||||
with:
|
||||
process-only: 'issues'
|
||||
|
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
|
|
36
.gitignore
vendored
36
.gitignore
vendored
|
@ -121,13 +121,11 @@ _artifacts
|
|||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
@ -140,6 +138,12 @@ project.fragment.lock.json
|
|||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
#VS outout folders
|
||||
bin
|
||||
obj
|
||||
output/*
|
||||
|
||||
|
||||
# macOS metadata files
|
||||
._*
|
||||
.DS_Store
|
||||
|
@ -158,12 +162,34 @@ Thumbs.db
|
|||
/tools/Addins/*
|
||||
packages.config.md5sum
|
||||
|
||||
|
||||
# Common IntelliJ Platform excludes
|
||||
|
||||
# User specific
|
||||
**/.idea/**/workspace.xml
|
||||
**/.idea/**/tasks.xml
|
||||
**/.idea/shelf/*
|
||||
**/.idea/dictionaries
|
||||
**/.idea/.idea.Radarr.Posix
|
||||
**/.idea/.idea.Radarr.Windows
|
||||
|
||||
# Sensitive or high-churn files
|
||||
**/.idea/**/dataSources/
|
||||
**/.idea/**/dataSources.ids
|
||||
**/.idea/**/dataSources.xml
|
||||
**/.idea/**/dataSources.local.xml
|
||||
**/.idea/**/sqlDataSources.xml
|
||||
**/.idea/**/dynamic.xml
|
||||
|
||||
# Rider
|
||||
# Rider auto-generates .iml files, and contentModel.xml
|
||||
**/.idea/**/*.iml
|
||||
**/.idea/**/contentModel.xml
|
||||
**/.idea/**/modules.xml
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||
.idea/
|
||||
|
|
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
|
||||
|
||||
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/lidarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
|
@ -9,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.
|
||||
|
||||
> [!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:
|
||||
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
|
|
|
@ -9,18 +9,18 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.13.3'
|
||||
majorVersion: '2.1.6'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.427'
|
||||
nodeVersion: '20.X'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-22.04'
|
||||
macImage: 'macOS-13'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
|
@ -166,10 +166,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: UseNode@1
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
version: $(nodeVersion)
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
@ -1093,10 +1093,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: UseNode@1
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
version: $(nodeVersion)
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
@ -1120,19 +1120,19 @@ stages:
|
|||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@3
|
||||
- task: SonarCloudPrepare@1
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'lidarr'
|
||||
scannerMode: 'cli'
|
||||
scannerMode: 'CLI'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'lidarr_Lidarr.UI'
|
||||
cliProjectName: 'LidarrUI'
|
||||
cliProjectVersion: '$(lidarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@3
|
||||
- task: SonarCloudAnalyze@1
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
|
@ -1208,12 +1208,12 @@ stages:
|
|||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@3
|
||||
- task: SonarCloudPrepare@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'lidarr'
|
||||
scannerMode: 'dotnet'
|
||||
scannerMode: 'MSBuild'
|
||||
projectKey: 'lidarr_Lidarr'
|
||||
projectName: 'Lidarr'
|
||||
projectVersion: '$(lidarrVersion)'
|
||||
|
@ -1226,16 +1226,21 @@ stages:
|
|||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@3
|
||||
- task: SonarCloudAnalyze@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5.3.11
|
||||
- task: reportgenerator@4
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
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
|
||||
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
|
||||
ARCHITECTURE="${2:-x64}"
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-$ARCHITECTURE"
|
||||
RUNTIME="win-x64"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
RUNTIME="linux-$ARCHITECTURE"
|
||||
RUNTIME="linux-x64"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
RUNTIME="osx-$ARCHITECTURE"
|
||||
RUNTIME="osx-x64"
|
||||
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
|
||||
fi
|
||||
|
||||
|
@ -26,21 +21,15 @@ slnFile=src/Lidarr.sln
|
|||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Lidarr.Console.dll
|
||||
else
|
||||
application=Lidarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -28,8 +28,7 @@ module.exports = {
|
|||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false,
|
||||
JSX: true
|
||||
sinon: false
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
|||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
"source.fixAll": true
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (env) => {
|
|||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
|
@ -68,7 +67,7 @@ module.exports = (env) => {
|
|||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
filename: '[name]-[contenthash].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
|
@ -93,7 +92,7 @@ module.exports = (env) => {
|
|||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
|
@ -135,12 +134,6 @@ module.exports = (env) => {
|
|||
{
|
||||
source: 'frontend/src/Content/robots.txt',
|
||||
destination: path.join(distFolder, 'Content/robots.txt')
|
||||
},
|
||||
|
||||
// manifest.json and browserconfig.xml
|
||||
{
|
||||
source: 'frontend/src/Content/*.(json|xml)',
|
||||
destination: path.join(distFolder, 'Content')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -188,7 +181,7 @@ module.exports = (env) => {
|
|||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: '3.41'
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -209,7 +202,7 @@ module.exports = (env) => {
|
|||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -16,7 +16,6 @@ const mixinsFiles = [
|
|||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
|
|
@ -172,8 +172,7 @@ function HistoryDetails(props) {
|
|||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message,
|
||||
indexer
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
|
@ -193,14 +192,6 @@ function HistoryDetails(props) {
|
|||
null
|
||||
}
|
||||
|
||||
{
|
||||
indexer ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{
|
||||
message ?
|
||||
<DescriptionListItem
|
||||
|
|
|
@ -26,5 +26,4 @@
|
|||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -57,40 +57,30 @@ function QueueStatusCell(props) {
|
|||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = translate('Paused');
|
||||
title = 'Paused';
|
||||
}
|
||||
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = translate('Queued');
|
||||
title = 'Queued';
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = translate('Downloaded');
|
||||
|
||||
if (trackedDownloadState === 'importBlocked') {
|
||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importFailed') {
|
||||
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
title = 'Downloaded';
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
title += ' - Waiting to Import';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ` - ${translate('Importing')}`;
|
||||
title += ' - Importing';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ` - ${translate('WaitingToProcess')}`;
|
||||
title += ' - Waiting to Process';
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
@ -101,38 +91,36 @@ function QueueStatusCell(props) {
|
|||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = translate('Pending');
|
||||
title = 'Pending';
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = translate('PendingDownloadClientUnavailable');
|
||||
title = 'Pending - Download client is unavailable';
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
title = 'Download failed';
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
const warningMessage =
|
||||
errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', { warningMessage });
|
||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('ImportFailed', { sourceTitle });
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
title = 'Download failed';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -118,7 +118,6 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
|||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
isDisabled: isPending,
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
|
@ -131,7 +130,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
|||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [isPending, multipleSelected]);
|
||||
}, [multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
|
|
|
@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
|
|||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllAlbums')}
|
||||
data={translate('MonitorAllAlbums')}
|
||||
data="Monitor all new albums"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('NewAlbums')}
|
||||
data={translate('MonitorNewAlbumsData')}
|
||||
data="Monitor new albums released after the newest existing album"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data={translate('MonitorNoAlbumsData')}
|
||||
data="Don't monitor any new albums"
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
|
|
|
@ -10,7 +10,6 @@ export interface Statistics {
|
|||
}
|
||||
|
||||
interface Album extends ModelBase {
|
||||
artistId: number;
|
||||
artist: Artist;
|
||||
foreignAlbumId: string;
|
||||
title: string;
|
||||
|
@ -20,7 +19,6 @@ interface Album extends ModelBase {
|
|||
monitored: boolean;
|
||||
releaseDate: string;
|
||||
statistics: Statistics;
|
||||
lastSearchTime?: string;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,10 @@ import Link from 'Components/Link/Link';
|
|||
|
||||
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
||||
const link = `/album/${foreignAlbumId}`;
|
||||
const albumTitle = `${title}${disambiguation ? ` (${disambiguation})` : ''}`;
|
||||
|
||||
return (
|
||||
<Link to={link} title={albumTitle}>
|
||||
{albumTitle}
|
||||
<Link to={link}>
|
||||
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
title,
|
||||
statistics = {},
|
||||
statistics,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
|
|
@ -121,8 +121,6 @@
|
|||
|
||||
.releaseDate,
|
||||
.sizeOnDisk,
|
||||
.albumType,
|
||||
.secondaryTypes,
|
||||
.qualityProfileName,
|
||||
.links,
|
||||
.tags {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
interface CssExports {
|
||||
'albumNavigationButton': string;
|
||||
'albumNavigationButtons': string;
|
||||
'albumType': string;
|
||||
'alternateTitlesIconContainer': string;
|
||||
'backdrop': string;
|
||||
'backdropOverlay': string;
|
||||
|
@ -21,7 +20,6 @@ interface CssExports {
|
|||
'overview': string;
|
||||
'qualityProfileName': string;
|
||||
'releaseDate': string;
|
||||
'secondaryTypes': string;
|
||||
'sizeOnDisk': string;
|
||||
'tags': string;
|
||||
'title': string;
|
||||
|
|
|
@ -192,7 +192,6 @@ class AlbumDetails extends Component {
|
|||
duration,
|
||||
overview,
|
||||
albumType,
|
||||
secondaryTypes,
|
||||
statistics = {},
|
||||
monitored,
|
||||
releaseDate,
|
||||
|
@ -205,7 +204,6 @@ class AlbumDetails extends Component {
|
|||
isFetching,
|
||||
isPopulated,
|
||||
albumsError,
|
||||
tracksError,
|
||||
trackFilesError,
|
||||
hasTrackFiles,
|
||||
shortDateFormat,
|
||||
|
@ -398,11 +396,10 @@ class AlbumDetails extends Component {
|
|||
<div className={styles.details}>
|
||||
<div>
|
||||
{
|
||||
duration ?
|
||||
!!duration &&
|
||||
<span className={styles.duration}>
|
||||
{formatDuration(duration)}
|
||||
</span> :
|
||||
null
|
||||
</span>
|
||||
}
|
||||
|
||||
<HeartRating
|
||||
|
@ -421,15 +418,14 @@ class AlbumDetails extends Component {
|
|||
title={translate('ReleaseDate')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.releaseDate}>
|
||||
{moment(releaseDate).format(shortDateFormat)}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.releaseDate}>
|
||||
{moment(releaseDate).format(shortDateFormat)}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
|
@ -438,15 +434,16 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -462,55 +459,32 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
{
|
||||
albumType ?
|
||||
!!albumType &&
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('Type')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.albumType}>
|
||||
{albumType}
|
||||
</span>
|
||||
</div>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
{
|
||||
secondaryTypes.length ?
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('SecondaryTypes')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.secondaryTypes}>
|
||||
{secondaryTypes.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</Label> :
|
||||
null
|
||||
<span className={styles.qualityProfileName}>
|
||||
{albumType}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
||||
<Tooltip
|
||||
|
@ -519,15 +493,14 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -553,9 +526,8 @@ class AlbumDetails extends Component {
|
|||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
!isPopulated && !albumsError && !trackFilesError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -566,14 +538,6 @@ class AlbumDetails extends Component {
|
|||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && tracksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('TracksLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && trackFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
|
@ -602,14 +566,6 @@ class AlbumDetails extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !media.length ?
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('NoMediumInformation')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
|
@ -676,7 +632,6 @@ AlbumDetails.propTypes = {
|
|||
duration: PropTypes.number,
|
||||
overview: PropTypes.string,
|
||||
albumType: PropTypes.string.isRequired,
|
||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
|
@ -703,8 +658,6 @@ AlbumDetails.propTypes = {
|
|||
};
|
||||
|
||||
AlbumDetails.defaultProps = {
|
||||
secondaryTypes: [],
|
||||
statistics: {},
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
|
|
|
@ -12,13 +12,16 @@ import TrackRowConnector from './TrackRowConnector';
|
|||
import styles from './AlbumDetailsMedium.css';
|
||||
|
||||
function getMediumStatistics(tracks) {
|
||||
const trackCount = tracks.length;
|
||||
let trackCount = 0;
|
||||
let trackFileCount = 0;
|
||||
let totalTrackCount = 0;
|
||||
|
||||
tracks.forEach((track) => {
|
||||
if (track.trackFileId) {
|
||||
trackCount++;
|
||||
trackFileCount++;
|
||||
} else {
|
||||
trackCount++;
|
||||
}
|
||||
|
||||
totalTrackCount++;
|
||||
|
@ -172,7 +175,7 @@ class AlbumDetailsMedium extends Component {
|
|||
</Table> :
|
||||
|
||||
<div className={styles.noTracks}>
|
||||
{translate('NoTracksInThisMedium')}
|
||||
No tracks in this medium
|
||||
</div>
|
||||
}
|
||||
<div className={styles.collapseButtonContainer}>
|
||||
|
|
|
@ -35,9 +35,3 @@
|
|||
|
||||
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;
|
||||
'customFormatScore': string;
|
||||
'duration': string;
|
||||
'indexerFlags': string;
|
||||
'monitored': string;
|
||||
'size': string;
|
||||
'status': string;
|
||||
|
|
|
@ -2,19 +2,15 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import AlbumFormats from 'Album/AlbumFormats';
|
||||
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
||||
import IndexerFlags from 'Album/IndexerFlags';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { tooltipPositions } from 'Helpers/Props';
|
||||
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
||||
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import TrackActionsCell from './TrackActionsCell';
|
||||
import styles from './TrackRow.css';
|
||||
|
||||
|
@ -36,7 +32,6 @@ class TrackRow extends Component {
|
|||
trackFileSize,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
indexerFlags,
|
||||
columns,
|
||||
deleteTrackFile
|
||||
} = this.props;
|
||||
|
@ -146,30 +141,12 @@ class TrackRow extends Component {
|
|||
customFormats.length
|
||||
)}
|
||||
tooltip={<AlbumFormats formats={customFormats} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</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') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
@ -231,14 +208,12 @@ TrackRow.propTypes = {
|
|||
trackFileSize: PropTypes.number,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
mediaInfo: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
TrackRow.defaultProps = {
|
||||
customFormats: [],
|
||||
indexerFlags: 0
|
||||
customFormats: []
|
||||
};
|
||||
|
||||
export default TrackRow;
|
||||
|
|
|
@ -13,8 +13,7 @@ function createMapStateToProps() {
|
|||
trackFilePath: trackFile ? trackFile.path : null,
|
||||
trackFileSize: trackFile ? trackFile.size : null,
|
||||
customFormats: trackFile ? trackFile.customFormats : [],
|
||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0,
|
||||
indexerFlags: trackFile ? trackFile.indexerFlags : 0
|
||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
|
|||
title,
|
||||
artistName,
|
||||
albumType,
|
||||
statistics = {},
|
||||
statistics,
|
||||
item,
|
||||
isSaving,
|
||||
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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -7,7 +7,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AlbumInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
|
@ -19,10 +18,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{albumTitle === undefined ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
|
||||
}
|
||||
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
|
@ -36,7 +32,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
@ -12,10 +12,11 @@ function App({ store, history }) {
|
|||
<DocumentTitle title={window.Lidarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
|
|
@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
|||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
|
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
|
@ -184,7 +184,7 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/settings/customformats"
|
||||
component={CustomFormatSettingsPage}
|
||||
component={CustomFormatSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
@ -248,7 +248,7 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={Updates}
|
||||
component={UpdatesConnector}
|
||||
/>
|
||||
|
||||
<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 ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
import TrackFilesAppState from './TrackFilesAppState';
|
||||
import TracksAppState from './TracksAppState';
|
||||
|
@ -44,7 +40,6 @@ export interface CustomFilter {
|
|||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
version: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
|
@ -57,16 +52,12 @@ interface AppState {
|
|||
app: AppSectionState;
|
||||
artist: ArtistAppState;
|
||||
artistIndex: ArtistIndexAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
parse: ParseAppState;
|
||||
queue: QueueAppState;
|
||||
settings: SettingsAppState;
|
||||
tags: TagsAppState;
|
||||
trackFiles: TrackFilesAppState;
|
||||
tracksSelection: TracksAppState;
|
||||
system: SystemAppState;
|
||||
}
|
||||
|
||||
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, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import MetadataProfile from 'typings/MetadataProfile';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import RootFolder from 'typings/RootFolder';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
|
@ -45,32 +39,22 @@ export interface MetadataProfilesAppState
|
|||
extends AppSectionState<MetadataProfile>,
|
||||
AppSectionSchemaState<MetadataProfile> {}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface RootFolderAppState
|
||||
extends AppSectionState<RootFolder>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
customFormats: CustomFormatAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
metadataProfiles: MetadataProfilesAppState;
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
ui: UiSettingsAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
}
|
||||
|
||||
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 AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface TagDetail extends ModelBase {
|
||||
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;
|
||||
}
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
|
||||
export default TagsAppState;
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface Ratings {
|
|||
|
||||
interface Artist extends ModelBase {
|
||||
added: string;
|
||||
artistMetadataId: string;
|
||||
foreignArtistId: string;
|
||||
cleanName: string;
|
||||
ended: boolean;
|
||||
|
@ -35,7 +36,6 @@ interface Artist extends ModelBase {
|
|||
nextAlbum?: Album;
|
||||
qualityProfileId: number;
|
||||
metadataProfileId: number;
|
||||
monitorNewItems: string;
|
||||
ratings: Ratings;
|
||||
rootFolderPath: string;
|
||||
sortName: string;
|
||||
|
|
|
@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteArtistConfirmed}
|
||||
>
|
||||
{translate('Delete')}
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
@ -161,7 +161,9 @@ DeleteArtistModalContent.propTypes = {
|
|||
};
|
||||
|
||||
DeleteArtistModalContent.defaultProps = {
|
||||
statistics: {}
|
||||
statistics: {
|
||||
trackFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default DeleteArtistModalContent;
|
||||
|
|
|
@ -10,7 +10,6 @@ function AlbumGroupInfo(props) {
|
|||
const {
|
||||
totalAlbumCount,
|
||||
monitoredAlbumCount,
|
||||
albumFileCount,
|
||||
trackFileCount,
|
||||
sizeOnDisk
|
||||
} = props;
|
||||
|
@ -31,13 +30,6 @@ function AlbumGroupInfo(props) {
|
|||
data={monitoredAlbumCount}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('WithFiles')}
|
||||
data={albumFileCount}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
|
@ -58,7 +50,6 @@ function AlbumGroupInfo(props) {
|
|||
AlbumGroupInfo.propTypes = {
|
||||
totalAlbumCount: PropTypes.number.isRequired,
|
||||
monitoredAlbumCount: PropTypes.number.isRequired,
|
||||
albumFileCount: PropTypes.number.isRequired,
|
||||
trackFileCount: PropTypes.number.isRequired,
|
||||
sizeOnDisk: PropTypes.number.isRequired
|
||||
};
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
width: 42px;
|
||||
}
|
||||
|
||||
.size,
|
||||
.status {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'monitored': string;
|
||||
'size': string;
|
||||
'status': string;
|
||||
'title': string;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AlbumRow.css';
|
||||
|
||||
|
@ -88,8 +87,7 @@ class AlbumRow extends Component {
|
|||
const {
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
totalTrackCount = 0,
|
||||
sizeOnDisk = 0
|
||||
totalTrackCount = 0
|
||||
} = statistics;
|
||||
|
||||
return (
|
||||
|
@ -149,7 +147,9 @@ class AlbumRow extends Component {
|
|||
if (name === 'secondaryTypes') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{secondaryTypes.join(', ')}
|
||||
{
|
||||
secondaryTypes
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ class AlbumRow extends Component {
|
|||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
totalTrackCount
|
||||
statistics.totalTrackCount
|
||||
}
|
||||
</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') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
|
|
@ -192,7 +192,7 @@ class ArtistDetails extends Component {
|
|||
artistName,
|
||||
ratings,
|
||||
path,
|
||||
statistics = {},
|
||||
statistics,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
genres,
|
||||
|
|
|
@ -22,43 +22,32 @@ import styles from './ArtistDetailsSeason.css';
|
|||
|
||||
function getAlbumStatistics(albums) {
|
||||
let albumCount = 0;
|
||||
let albumFileCount = 0;
|
||||
let trackFileCount = 0;
|
||||
let totalAlbumCount = 0;
|
||||
let monitoredAlbumCount = 0;
|
||||
let hasMonitoredAlbums = false;
|
||||
let sizeOnDisk = 0;
|
||||
|
||||
albums.forEach(({ monitored, releaseDate, statistics = {} }) => {
|
||||
const {
|
||||
trackFileCount: albumTrackFileCount = 0,
|
||||
totalTrackCount: albumTotalTrackCount = 0,
|
||||
sizeOnDisk: albumSizeOnDisk = 0
|
||||
} = statistics;
|
||||
albums.forEach((album) => {
|
||||
if (album.statistics) {
|
||||
sizeOnDisk = sizeOnDisk + album.statistics.sizeOnDisk;
|
||||
trackFileCount = trackFileCount + album.statistics.trackFileCount;
|
||||
|
||||
const hasFiles = albumTrackFileCount > 0 && albumTrackFileCount === albumTotalTrackCount;
|
||||
|
||||
if (hasFiles || (monitored && isBefore(releaseDate))) {
|
||||
albumCount++;
|
||||
if (album.statistics.trackFileCount === album.statistics.totalTrackCount || (album.monitored && isBefore(album.airDateUtc))) {
|
||||
albumCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFiles) {
|
||||
albumFileCount++;
|
||||
}
|
||||
|
||||
if (monitored) {
|
||||
if (album.monitored) {
|
||||
monitoredAlbumCount++;
|
||||
hasMonitoredAlbums = true;
|
||||
}
|
||||
|
||||
totalAlbumCount++;
|
||||
trackFileCount = trackFileCount + albumTrackFileCount;
|
||||
sizeOnDisk = sizeOnDisk + albumSizeOnDisk;
|
||||
});
|
||||
|
||||
return {
|
||||
albumCount,
|
||||
albumFileCount,
|
||||
totalAlbumCount,
|
||||
trackFileCount,
|
||||
monitoredAlbumCount,
|
||||
|
@ -67,8 +56,8 @@ function getAlbumStatistics(albums) {
|
|||
};
|
||||
}
|
||||
|
||||
function getAlbumCountKind(monitored, albumCount, albumFileCount) {
|
||||
if (albumCount === albumFileCount && albumFileCount > 0) {
|
||||
function getAlbumCountKind(monitored, albumCount, monitoredAlbumCount) {
|
||||
if (albumCount === monitoredAlbumCount && monitoredAlbumCount > 0) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
|
@ -203,12 +192,11 @@ class ArtistDetailsSeason extends Component {
|
|||
|
||||
const {
|
||||
albumCount,
|
||||
albumFileCount,
|
||||
totalAlbumCount,
|
||||
trackFileCount,
|
||||
monitoredAlbumCount,
|
||||
hasMonitoredAlbums,
|
||||
sizeOnDisk = 0
|
||||
sizeOnDisk
|
||||
} = getAlbumStatistics(items);
|
||||
|
||||
const {
|
||||
|
@ -238,9 +226,9 @@ class ArtistDetailsSeason extends Component {
|
|||
anchor={
|
||||
<Label
|
||||
size={sizes.LARGE}
|
||||
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, albumFileCount)}
|
||||
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, monitoredAlbumCount)}
|
||||
>
|
||||
<span>{albumFileCount} / {albumCount}</span>
|
||||
<span>{albumCount} / {monitoredAlbumCount}</span>
|
||||
</Label>
|
||||
}
|
||||
title={translate('GroupInformation')}
|
||||
|
@ -249,7 +237,6 @@ class ArtistDetailsSeason extends Component {
|
|||
<AlbumGroupInfo
|
||||
totalAlbumCount={totalAlbumCount}
|
||||
monitoredAlbumCount={monitoredAlbumCount}
|
||||
albumFileCount={albumFileCount}
|
||||
trackFileCount={trackFileCount}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
/>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import ArtistTags from './ArtistTags';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -13,8 +12,8 @@ function createMapStateToProps() {
|
|||
const tags = artist.tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((tag) => !!tag)
|
||||
.sort(sortByProp('label'))
|
||||
.map((tag) => tag.label);
|
||||
.map((tag) => tag.label)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return {
|
||||
tags
|
||||
|
|
|
@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditArtistModalContent.css';
|
||||
|
||||
|
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
|
|||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Monitored')}
|
||||
</FormLabel>
|
||||
|
@ -107,10 +107,9 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewItems')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
|
@ -133,7 +132,7 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('QualityProfile')}
|
||||
</FormLabel>
|
||||
|
@ -147,10 +146,10 @@ class EditArtistModalContent extends Component {
|
|||
</FormGroup>
|
||||
|
||||
{
|
||||
showMetadataProfile ?
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
showMetadataProfile &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MetadataProfile')}
|
||||
Metadata Profile
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
|
@ -174,11 +173,10 @@ class EditArtistModalContent extends Component {
|
|||
{...metadataProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Path')}
|
||||
</FormLabel>
|
||||
|
@ -191,7 +189,7 @@ class EditArtistModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Tags')}
|
||||
</FormLabel>
|
||||
|
@ -211,7 +209,7 @@ class EditArtistModalContent extends Component {
|
|||
kind={kinds.DANGER}
|
||||
onPress={onDeleteArtistPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
|
|
|
@ -7,8 +7,6 @@ import React, {
|
|||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import { RSS_SYNC } from 'Commands/commandNames';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
@ -91,19 +89,16 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
}: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState =
|
||||
useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
|
||||
} = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
|
||||
|
||||
const isRssSyncExecuting = useSelector(
|
||||
createCommandExecutingSelector(RSS_SYNC)
|
||||
);
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
const dispatch = useDispatch();
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollerRef = useRef<HTMLDivElement>();
|
||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -123,14 +118,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
}, [isSelectMode, setIsSelectMode]);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload: unknown) => {
|
||||
(payload) => {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onViewSelect = useCallback(
|
||||
(value: string) => {
|
||||
(value) => {
|
||||
dispatch(setArtistView({ view: value }));
|
||||
|
||||
if (scrollerRef.current) {
|
||||
|
@ -141,14 +136,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
);
|
||||
|
||||
const onSortSelect = useCallback(
|
||||
(value: string) => {
|
||||
(value) => {
|
||||
dispatch(setArtistSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value: string) => {
|
||||
(value) => {
|
||||
dispatch(setArtistFilter({ selectedFilterKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -163,15 +158,15 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
}, [setIsOptionsModalOpen]);
|
||||
|
||||
const onJumpBarItemPress = useCallback(
|
||||
(character: string) => {
|
||||
(character) => {
|
||||
setJumpToCharacter(character);
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
({ scrollTop }: { scrollTop: number }) => {
|
||||
setJumpToCharacter(undefined);
|
||||
({ scrollTop }) => {
|
||||
setJumpToCharacter(null);
|
||||
scrollPositions.artistIndex = scrollTop;
|
||||
},
|
||||
[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);
|
||||
|
||||
if (!isNaN(Number(char))) {
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
|
@ -305,8 +300,6 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
<PageContentBody
|
||||
ref={scrollerRef}
|
||||
className={styles.contentBody}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
initialScrollTop={props.initialScrollTop}
|
||||
onScroll={onScroll}
|
||||
|
|
|
@ -23,13 +23,7 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
interface ArtistIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function ArtistIndexFilterModal(
|
||||
props: ArtistIndexFilterModalProps
|
||||
) {
|
||||
export default function ArtistIndexFilterModal(props) {
|
||||
const sectionItems = useSelector(createArtistSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'artist';
|
||||
|
@ -37,7 +31,7 @@ export default function ArtistIndexFilterModal(
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
(payload) => {
|
||||
dispatch(setArtistFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -45,7 +39,6 @@ export default function ArtistIndexFilterModal(
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
|
|
@ -206,7 +206,7 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
{showQualityProfile ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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 { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
|
@ -22,7 +21,7 @@ const columnPaddingSmallScreen = parseInt(
|
|||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
|
||||
const ADDITIONAL_COLUMN_COUNT = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1,
|
||||
|
@ -42,17 +41,17 @@ interface CellItemData {
|
|||
|
||||
interface ArtistIndexBannersProps {
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const artistIndexSelector = createSelector(
|
||||
(state: AppState) => state.artistIndex.bannerOptions,
|
||||
(state) => state.artistIndex.bannerOptions,
|
||||
(bannerOptions) => {
|
||||
return {
|
||||
bannerOptions,
|
||||
|
@ -109,7 +108,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
|||
} = props;
|
||||
|
||||
const { bannerOptions } = useSelector(artistIndexSelector);
|
||||
const ref = useRef<Grid>(null);
|
||||
const ref: React.MutableRefObject<Grid> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
@ -223,8 +222,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
|||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -233,7 +232,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -256,8 +255,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
|||
|
||||
const scrollTop = rowIndex * rowHeight + padding;
|
||||
|
||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
|
|
@ -59,7 +59,7 @@ function ArtistIndexBannerOptionsModalContent(
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const onBannerOptionChange = useCallback(
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
({ name, value }) => {
|
||||
dispatch(setArtistBannerOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { CustomFilter } from 'App/State/AppState';
|
||||
import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
interface ArtistIndexFilterMenuProps {
|
||||
selectedFilterKey: string | number;
|
||||
filters: object[];
|
||||
customFilters: CustomFilter[];
|
||||
isDisabled: boolean;
|
||||
onFilterSelect(filterName: string): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) {
|
||||
function ArtistIndexFilterMenu(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
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 = {
|
||||
showCustomFilters: false,
|
||||
};
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SortMenu from 'Components/Menu/SortMenu';
|
||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
|
||||
interface SeriesIndexSortMenuProps {
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
isDisabled: boolean;
|
||||
onSortSelect(sortKey: string): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||
function ArtistIndexSortMenu(props) {
|
||||
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
|
||||
|
||||
return (
|
||||
|
@ -25,7 +17,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('MonitoredStatus')}
|
||||
Monitored/Status
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -34,7 +26,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Name')}
|
||||
Name
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -43,7 +35,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Type')}
|
||||
Type
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -52,7 +44,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('QualityProfile')}
|
||||
Quality Profile
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -61,7 +53,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('MetadataProfile')}
|
||||
Metadata Profile
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -70,7 +62,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('NextAlbum')}
|
||||
Next Album
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -79,7 +71,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('LastAlbum')}
|
||||
Last Album
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -88,7 +80,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Added')}
|
||||
Added
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -97,7 +89,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Albums')}
|
||||
Albums
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -106,7 +98,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Tracks')}
|
||||
Tracks
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -115,7 +107,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('TrackCount')}
|
||||
Track Count
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -124,7 +116,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Path')}
|
||||
Path
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -133,7 +125,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('SizeOnDisk')}
|
||||
Size on Disk
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -142,11 +134,18 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Tags')}
|
||||
Tags
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexSortMenu.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ArtistIndexSortMenu;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import ViewMenu from 'Components/Menu/ViewMenu';
|
||||
|
@ -5,13 +6,7 @@ import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
|||
import { align } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface ArtistIndexViewMenuProps {
|
||||
view: string;
|
||||
isDisabled: boolean;
|
||||
onViewSelect(value: string): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) {
|
||||
function ArtistIndexViewMenu(props) {
|
||||
const { view, isDisabled, onViewSelect } = props;
|
||||
|
||||
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;
|
||||
|
|
|
@ -1,51 +1,15 @@
|
|||
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Album from 'Album/Album';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
|
||||
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 rows = [
|
||||
|
@ -86,17 +50,11 @@ const rows = [
|
|||
},
|
||||
];
|
||||
|
||||
function getInfoRowProps(
|
||||
row: RowProps,
|
||||
props: ArtistIndexOverviewInfoProps,
|
||||
uiSettings: UiSettings
|
||||
): RowInfoProps | null {
|
||||
function getInfoRowProps(row, props, uiSettings) {
|
||||
const { name } = row;
|
||||
|
||||
if (name === 'monitored') {
|
||||
const monitoredText = props.monitored
|
||||
? translate('Monitored')
|
||||
: translate('Unmonitored');
|
||||
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
||||
|
||||
return {
|
||||
title: monitoredText,
|
||||
|
@ -105,9 +63,9 @@ function getInfoRowProps(
|
|||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: translate('QualityProfile'),
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name,
|
||||
};
|
||||
|
@ -120,16 +78,15 @@ function getInfoRowProps(
|
|||
return {
|
||||
title: `Last Album: ${lastAlbum.title}`,
|
||||
iconName: icons.CALENDAR,
|
||||
label:
|
||||
getRelativeDate(
|
||||
lastAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
) ?? '',
|
||||
label: getRelativeDate(
|
||||
lastAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -141,11 +98,10 @@ function getInfoRowProps(
|
|||
return {
|
||||
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
||||
iconName: icons.ADD,
|
||||
label:
|
||||
getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}) ?? '',
|
||||
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -160,7 +116,7 @@ function getInfoRowProps(
|
|||
}
|
||||
|
||||
return {
|
||||
title: translate('AlbumCount'),
|
||||
title: 'Album Count',
|
||||
iconName: icons.CIRCLE,
|
||||
label: albums,
|
||||
};
|
||||
|
@ -168,7 +124,7 @@ function getInfoRowProps(
|
|||
|
||||
if (name === 'path') {
|
||||
return {
|
||||
title: translate('Path'),
|
||||
title: 'Path',
|
||||
iconName: icons.FOLDER,
|
||||
label: props.path,
|
||||
};
|
||||
|
@ -176,13 +132,31 @@ function getInfoRowProps(
|
|||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return {
|
||||
title: translate('SizeOnDisk'),
|
||||
title: 'Size on Disk',
|
||||
iconName: icons.DRIVE,
|
||||
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) {
|
||||
|
@ -201,8 +175,6 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
|||
const { name, showProp, valueProp } = row;
|
||||
|
||||
const isVisible =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(7053)
|
||||
props[valueProp] != null && (props[showProp] || props.sortKey === name);
|
||||
|
||||
return {
|
||||
|
@ -247,10 +219,6 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
|||
|
||||
const infoRowProps = getInfoRowProps(row, props, uiSettings);
|
||||
|
||||
if (infoRowProps == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
|
||||
})}
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './ArtistIndexOverviewInfoRow.css';
|
||||
|
||||
interface ArtistIndexOverviewInfoRowProps {
|
||||
title?: string;
|
||||
iconName?: IconDefinition;
|
||||
label: string | null;
|
||||
iconName: object;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import Artist from 'Artist/Artist';
|
||||
|
@ -33,11 +33,11 @@ interface RowItemData {
|
|||
|
||||
interface ArtistIndexOverviewsProps {
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
sortKey?: string;
|
||||
sortDirection?: string;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
|||
const { size: posterSize, detailedProgressBar } = useSelector(
|
||||
selectOverviewOptions
|
||||
);
|
||||
const listRef = useRef<List>(null);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
@ -136,8 +136,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
|||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -146,7 +146,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -175,8 +175,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
|||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
|
|
@ -60,7 +60,7 @@ function ArtistIndexOverviewOptionsModalContent(
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const onOverviewOptionChange = useCallback(
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
({ name, value }) => {
|
||||
dispatch(setArtistOverviewOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
{showQualityProfile ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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 { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
|
@ -22,7 +21,7 @@ const columnPaddingSmallScreen = parseInt(
|
|||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
|
||||
const ADDITIONAL_COLUMN_COUNT = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1,
|
||||
|
@ -42,17 +41,17 @@ interface CellItemData {
|
|||
|
||||
interface ArtistIndexPostersProps {
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const artistIndexSelector = createSelector(
|
||||
(state: AppState) => state.artistIndex.posterOptions,
|
||||
(state) => state.artistIndex.posterOptions,
|
||||
(posterOptions) => {
|
||||
return {
|
||||
posterOptions,
|
||||
|
@ -109,7 +108,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
} = props;
|
||||
|
||||
const { posterOptions } = useSelector(artistIndexSelector);
|
||||
const ref = useRef<Grid>(null);
|
||||
const ref: React.MutableRefObject<Grid> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
@ -232,8 +231,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -242,7 +241,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -265,8 +264,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
|
||||
const scrollTop = rowIndex * rowHeight + padding;
|
||||
|
||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
|
|
@ -59,7 +59,7 @@ function ArtistIndexPosterOptionsModalContent(
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const onPosterOptionChange = useCallback(
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
({ name, value }) => {
|
||||
dispatch(setArtistPosterOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Statistics } from 'Album/Album';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
@ -57,8 +56,8 @@ function AlbumDetails(props: AlbumDetailsProps) {
|
|||
disambiguation,
|
||||
albumType,
|
||||
monitored,
|
||||
statistics = {} as Statistics,
|
||||
isSaving = false,
|
||||
statistics,
|
||||
isSaving,
|
||||
} = album;
|
||||
|
||||
return (
|
||||
|
|
|
@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps {
|
|||
artistId: number;
|
||||
albumId: number;
|
||||
title: string;
|
||||
disambiguation?: string;
|
||||
disambiguation: string;
|
||||
albumType: string;
|
||||
monitored: boolean;
|
||||
statistics: Statistics;
|
||||
|
|
|
@ -33,7 +33,7 @@ function ChangeMonitoringModalContent(
|
|||
const [monitor, setMonitor] = useState(NO_CHANGE);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
({ value }) => {
|
||||
setMonitor(value);
|
||||
},
|
||||
[setMonitor]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { SyntheticEvent, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
@ -15,9 +15,8 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
|
|||
const isSelected = selectState.selectedState[artistId];
|
||||
|
||||
const onSelectPress = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
const nativeEvent = event.nativeEvent as PointerEvent;
|
||||
const shiftKey = nativeEvent.shiftKey;
|
||||
(event) => {
|
||||
const shiftKey = event.nativeEvent.shiftKey;
|
||||
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
|||
interface ArtistIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
}
|
||||
|
||||
function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {
|
||||
|
|
|
@ -24,14 +24,6 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal';
|
|||
import TagsModal from './Tags/TagsModal';
|
||||
import styles from './ArtistIndexSelectFooter.css';
|
||||
|
||||
interface SavePayload {
|
||||
monitored?: boolean;
|
||||
qualityProfileId?: number;
|
||||
metadataProfileId?: number;
|
||||
rootFolderPath?: string;
|
||||
moveFiles?: boolean;
|
||||
}
|
||||
|
||||
const artistEditorSelector = createSelector(
|
||||
(state: AppState) => state.artist,
|
||||
(artist) => {
|
||||
|
@ -87,7 +79,7 @@ function ArtistIndexSelectFooter() {
|
|||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onSavePress = useCallback(
|
||||
(payload: SavePayload) => {
|
||||
(payload) => {
|
||||
setIsSavingArtist(true);
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
|
@ -126,7 +118,7 @@ function ArtistIndexSelectFooter() {
|
|||
}, [setIsTagsModalOpen]);
|
||||
|
||||
const onApplyTagsPress = useCallback(
|
||||
(tags: number[], applyTags: string) => {
|
||||
(tags, applyTags) => {
|
||||
setIsSavingTags(true);
|
||||
setIsTagsModalOpen(false);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps {
|
|||
label: string;
|
||||
iconName: IconDefinition;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,15 +28,9 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const artistNames = useMemo(() => {
|
||||
const artists = artistIds.reduce((acc: Artist[], id) => {
|
||||
const a = allArtists.find((a) => a.id === id);
|
||||
|
||||
if (a) {
|
||||
acc.push(a);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
const artists = artistIds.map((id) => {
|
||||
return allArtists.find((a) => a.id === id);
|
||||
});
|
||||
|
||||
const sorted = orderBy(artists, ['sortName']);
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
|||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteArtistModalContent.css';
|
||||
|
||||
|
@ -38,16 +37,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) {
|
|||
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const artists = useMemo((): Artist[] => {
|
||||
const artistList = artistIds.map((id) => {
|
||||
const artists = useMemo(() => {
|
||||
const artists = artistIds.map((id) => {
|
||||
return allArtists.find((a) => a.id === id);
|
||||
}) as Artist[];
|
||||
});
|
||||
|
||||
return orderBy(artistList, ['sortName']);
|
||||
return orderBy(artists, ['sortName']);
|
||||
}, [artistIds, allArtists]);
|
||||
|
||||
const onDeleteFilesChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
({ value }) => {
|
||||
setDeleteFiles(value);
|
||||
},
|
||||
[setDeleteFiles]
|
||||
|
|
|
@ -35,7 +35,7 @@ const monitoredOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: true,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
|
@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
|
|||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||
|
||||
const save = useCallback(
|
||||
(moveFiles: boolean) => {
|
||||
(moveFiles) => {
|
||||
let hasChanges = false;
|
||||
const payload: SavePayload = {};
|
||||
|
||||
|
@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
|
|||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }: { name: string; value: string }) => {
|
||||
({ name, value }) => {
|
||||
switch (name) {
|
||||
case 'monitored':
|
||||
setMonitored(value);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { uniq } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
|
@ -29,7 +28,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
|||
const { artistIds, onModalClose, onApplyTagsPress } = props;
|
||||
|
||||
const allArtists: Artist[] = useSelector(createAllArtistSelector());
|
||||
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
|
||||
const [tags, setTags] = useState<number[]>([]);
|
||||
const [applyTags, setApplyTags] = useState('add');
|
||||
|
@ -49,14 +48,14 @@ function TagsModalContent(props: TagsModalContentProps) {
|
|||
}, [artistIds, allArtists]);
|
||||
|
||||
const onTagsChange = useCallback(
|
||||
({ value }: { value: number[] }) => {
|
||||
({ value }) => {
|
||||
setTags(value);
|
||||
},
|
||||
[setTags]
|
||||
);
|
||||
|
||||
const onApplyTagsChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
({ value }) => {
|
||||
setApplyTags(value);
|
||||
},
|
||||
[setApplyTags]
|
||||
|
|
|
@ -67,7 +67,6 @@
|
|||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.monitorNewItems,
|
||||
.nextAlbum,
|
||||
.lastAlbum,
|
||||
.added,
|
||||
|
|
|
@ -14,7 +14,6 @@ interface CssExports {
|
|||
'lastAlbum': string;
|
||||
'link': string;
|
||||
'metadataProfileId': string;
|
||||
'monitorNewItems': string;
|
||||
'nextAlbum': string;
|
||||
'overlayTitle': string;
|
||||
'path': string;
|
||||
|
|
|
@ -23,9 +23,7 @@ import Column from 'Components/Table/Column';
|
|||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumsCell from './AlbumsCell';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
|
@ -58,7 +56,6 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
monitored,
|
||||
status,
|
||||
path,
|
||||
monitorNewItems,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
added,
|
||||
|
@ -129,7 +126,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
||||
({ id, value, shiftKey }) => {
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
id,
|
||||
|
@ -220,7 +217,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{qualityProfile?.name ?? ''}
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -228,15 +233,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{metadataProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'monitorNewItems') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{translate(firstCharToUpper(monitorNewItems))}
|
||||
{metadataProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -255,7 +252,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
}
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{translate('None')}
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -274,15 +271,13 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
}
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{translate('None')}
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
|
@ -333,7 +328,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
if (name === 'path') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<span title={path}>{path}</span>
|
||||
{path}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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 { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
|
||||
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
|
||||
|
@ -31,17 +30,17 @@ interface RowItemData {
|
|||
|
||||
interface ArtistIndexTableProps {
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const columnsSelector = createSelector(
|
||||
(state: AppState) => state.artistIndex.columns,
|
||||
(state) => state.artistIndex.columns,
|
||||
(columns) => columns
|
||||
);
|
||||
|
||||
|
@ -94,7 +93,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
|
||||
const columns = useSelector(columnsSelector);
|
||||
const { showBanners } = useSelector(selectTableOptions);
|
||||
const listRef = useRef<List<RowItemData>>(null);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
const windowWidth = window.innerWidth;
|
||||
|
@ -105,7 +104,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
}, [showBanners]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef?.current as HTMLElement;
|
||||
const current = scrollerRef.current as HTMLElement;
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSize({
|
||||
|
@ -129,8 +128,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -139,7 +138,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -168,8 +167,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
scrollerRef?.current?.scrollTo(0, scrollTop);
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.monitorNewItems,
|
||||
.nextAlbum,
|
||||
.lastAlbum,
|
||||
.added,
|
||||
|
|
|
@ -11,7 +11,6 @@ interface CssExports {
|
|||
'lastAlbum': string;
|
||||
'latestAlbum': string;
|
||||
'metadataProfileId': string;
|
||||
'monitorNewItems': string;
|
||||
'nextAlbum': string;
|
||||
'path': string;
|
||||
'qualityProfileId': string;
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
setArtistSort,
|
||||
setArtistTableOption,
|
||||
} from 'Store/Actions/artistIndexActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import styles from './ArtistIndexTableHeader.css';
|
||||
|
||||
|
@ -33,21 +32,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
|||
const [selectState, selectDispatch] = useSelect();
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(value: string) => {
|
||||
(value) => {
|
||||
dispatch(setArtistSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload: unknown) => {
|
||||
(payload) => {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
({ value }) => {
|
||||
selectDispatch({
|
||||
type: value ? 'selectAll' : 'unselectAll',
|
||||
});
|
||||
|
@ -95,8 +94,6 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
|||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
styles[name],
|
||||
name === 'sortName' && showBanners && styles.banner,
|
||||
name === 'sortName' &&
|
||||
|
|
|
@ -4,7 +4,6 @@ import FormGroup from 'Components/Form/FormGroup';
|
|||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
|
||||
|
@ -20,7 +19,7 @@ function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) {
|
|||
const { showBanners, showSearchAction } = tableOptions;
|
||||
|
||||
const onTableOptionChangeWrapper = useCallback(
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
({ name, value }) => {
|
||||
onTableOptionChange({
|
||||
tableOptions: {
|
||||
...tableOptions,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import Artist from 'Artist/Artist';
|
||||
import Command from 'Commands/Command';
|
||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
||||
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
||||
|
@ -13,21 +12,25 @@ function createArtistIndexItemSelector(artistId: number) {
|
|||
createArtistQualityProfileSelector(artistId),
|
||||
createArtistMetadataProfileSelector(artistId),
|
||||
createExecutingCommandsSelector(),
|
||||
(
|
||||
artist: Artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
executingCommands: Command[]
|
||||
) => {
|
||||
(artist: Artist, qualityProfile, metadataProfile, executingCommands) => {
|
||||
// If an artist is deleted this selector may fire before the parent
|
||||
// selectors, which will result in an undefined artist, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show an artist that has no information available.
|
||||
|
||||
if (!artist) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isRefreshingArtist = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === REFRESH_ARTIST && command.body.artistId === artistId
|
||||
command.name === REFRESH_ARTIST && command.body.artistId === artist.id
|
||||
);
|
||||
});
|
||||
|
||||
const isSearchingArtist = executingCommands.some((command) => {
|
||||
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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
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 translate from 'Utilities/String/translate';
|
||||
import CalendarConnector from './CalendarConnector';
|
||||
import CalendarFilterModal from './CalendarFilterModal';
|
||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||
import LegendConnector from './Legend/LegendConnector';
|
||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||
|
@ -79,7 +78,6 @@ class CalendarPage extends Component {
|
|||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
hasArtist,
|
||||
artistError,
|
||||
artistIsFetching,
|
||||
|
@ -139,8 +137,7 @@ class CalendarPage extends Component {
|
|||
isDisabled={!hasArtist}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={CalendarFilterModal}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
@ -207,7 +204,6 @@ class CalendarPage extends Component {
|
|||
CalendarPage.propTypes = {
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasArtist: PropTypes.bool.isRequired,
|
||||
artistError: PropTypes.object,
|
||||
artistIsFetching: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -6,7 +6,6 @@ import withCurrentPage from 'Components/withCurrentPage';
|
|||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
@ -60,7 +59,6 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
(state) => state.calendar.selectedFilterKey,
|
||||
(state) => state.calendar.filters,
|
||||
createCustomFiltersSelector('calendar'),
|
||||
createArtistCountSelector(),
|
||||
createUISettingsSelector(),
|
||||
createMissingAlbumIdsSelector(),
|
||||
|
@ -69,7 +67,6 @@ function createMapStateToProps() {
|
|||
(
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
artistCount,
|
||||
uiSettings,
|
||||
missingAlbumIds,
|
||||
|
@ -79,7 +76,6 @@ function createMapStateToProps() {
|
|||
return {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
hasArtist: !!artistCount.count,
|
||||
artistError: artistCount.error,
|
||||
|
|
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
|||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
value={tags}
|
||||
helpText={translate('ICalTagsArtistHelpText')}
|
||||
helpText={translate('TagsHelpText')}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</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