mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 13:10:13 -07:00
Compare commits
No commits in common. "develop" and "v2.1.1.3877" have entirely different histories.
develop
...
v2.1.1.387
928 changed files with 11307 additions and 27581 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.1'
|
||||
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",
|
||||
|
|
|
@ -2,8 +2,6 @@ const loose = true;
|
|||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,10 +3,9 @@ import React from 'react';
|
|||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryEventTypeCell.css';
|
||||
|
||||
function getIconName(eventType, data) {
|
||||
function getIconName(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return icons.DOWNLOADING;
|
||||
|
@ -17,7 +16,7 @@ function getIconName(eventType, data) {
|
|||
case 'downloadFailed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'trackFileDeleted':
|
||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||
return icons.DELETE;
|
||||
case 'trackFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'trackFileRetagged':
|
||||
|
@ -55,11 +54,11 @@ function getTooltip(eventType, data) {
|
|||
case 'downloadFailed':
|
||||
return 'Album download failed';
|
||||
case 'trackFileDeleted':
|
||||
return data.reason === 'MissingFromDisk' ? translate('TrackFileMissingTooltip') : translate('TrackFileDeletedTooltip');
|
||||
return 'Track file deleted';
|
||||
case 'trackFileRenamed':
|
||||
return translate('TrackFileRenamedTooltip');
|
||||
return 'Track file renamed';
|
||||
case 'trackFileRetagged':
|
||||
return translate('TrackFileTagsUpdatedTooltip');
|
||||
return 'Track file tags updated';
|
||||
case 'albumImportIncomplete':
|
||||
return 'Files downloaded but not all could be imported';
|
||||
case 'downloadImported':
|
||||
|
@ -72,7 +71,7 @@ function getTooltip(eventType, data) {
|
|||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, data }) {
|
||||
const iconName = getIconName(eventType, data);
|
||||
const iconName = getIconName(eventType);
|
||||
const iconKind = getIconKind(eventType);
|
||||
const tooltip = getTooltip(eventType, data);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
|||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
|
@ -309,16 +309,9 @@ class Queue extends Component {
|
|||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
@ -326,7 +319,7 @@ class Queue extends Component {
|
|||
return !!(item && item.artistId && item.albumId);
|
||||
})
|
||||
)}
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
|
|
|
@ -26,5 +26,4 @@
|
|||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -98,10 +98,8 @@ class QueueRow extends Component {
|
|||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
downloadForced,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
|
@ -330,15 +328,6 @@ class QueueRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
@ -404,7 +393,6 @@ class QueueRow extends Component {
|
|||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!artist}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
|
@ -434,10 +422,8 @@ QueueRow.propTypes = {
|
|||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
downloadForced: PropTypes.bool.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
sizeleft: PropTypes.number,
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
175
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
175
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
removeFromClient: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
removeFromClient: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveFromClientChange = ({ value }) => {
|
||||
this.setState({ removeFromClient: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { removeFromClient, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove - {sourceTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFromClient"
|
||||
value={removeFromClient}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveFromClientChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
|
@ -1,231 +0,0 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
removeFromClient: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
skipRedownload: boolean;
|
||||
}
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
selectedCount?: number;
|
||||
onRemovePress(props: RemovePressProps): void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
selectedCount,
|
||||
onRemovePress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedCount === 1) {
|
||||
return {
|
||||
title: translate('RemoveSelectedItem'),
|
||||
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: translate('RemoveSelectedItems'),
|
||||
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||
selectedCount,
|
||||
}),
|
||||
};
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
hint: multipleSelected
|
||||
? translate('RemoveMultipleFromDownloadClientHint')
|
||||
: translate('RemoveFromDownloadClientHint'),
|
||||
},
|
||||
{
|
||||
key: 'changeCategory',
|
||||
value: translate('ChangeCategory'),
|
||||
isDisabled: !canChangeCategory,
|
||||
hint: multipleSelected
|
||||
? translate('ChangeCategoryMultipleHint')
|
||||
: translate('ChangeCategoryHint'),
|
||||
},
|
||||
{
|
||||
key: 'ignore',
|
||||
value: multipleSelected
|
||||
? translate('IgnoreDownloads')
|
||||
: translate('IgnoreDownload'),
|
||||
isDisabled: !canIgnore,
|
||||
hint: multipleSelected
|
||||
? translate('IgnoreDownloadsHint')
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
hint: translate('DoNotBlocklistHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
isDisabled: isPending,
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [isPending, multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
removeFromClient: removalMethod === 'removeFromClient',
|
||||
changeCategory: removalMethod === 'changeCategory',
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>{message}</div>
|
||||
|
||||
{isPending ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="removalMethod"
|
||||
value={removalMethod}
|
||||
values={removalMethodOptions}
|
||||
isDisabled={!canChangeCategory && !canIgnore}
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{multipleSelected
|
||||
? translate('BlocklistReleases')
|
||||
: translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="blocklistMethod"
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveQueueItemModal;
|
176
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
176
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
removeFromClient: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
removeFromClient: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveFromClientChange = ({ value }) => {
|
||||
this.setState({ removeFromClient: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { removeFromClient, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', [selectedCount]) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFromClient"
|
||||
value={removeFromClient}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveFromClientChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
|
@ -1,9 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
|
@ -28,13 +25,11 @@ function TimeleftCell(props) {
|
|||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={translate('DelayingDownloadUntil', { date, time })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('DelayingDownloadUntilInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -44,13 +39,11 @@ function TimeleftCell(props) {
|
|||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={translate('RetryingDownloadOn', { date, time })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('RetryingDownloadInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,7 +4,6 @@ import IconButton from 'Components/Link/IconButton';
|
|||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumInteractiveSearchModalConnector from './Search/AlbumInteractiveSearchModalConnector';
|
||||
import styles from './AlbumSearchCell.css';
|
||||
|
||||
|
@ -50,13 +49,11 @@ class AlbumSearchCell extends Component {
|
|||
name={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
onPress={onSearchPress}
|
||||
title={translate('AutomaticSearch')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onManualSearchPress}
|
||||
title={translate('InteractiveSearch')}
|
||||
/>
|
||||
|
||||
<AlbumInteractiveSearchModalConnector
|
||||
|
|
|
@ -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,13 +53,13 @@ class DeleteAlbumModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
title,
|
||||
statistics = {},
|
||||
statistics,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
trackFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
|
@ -133,14 +133,14 @@ class DeleteAlbumModalContent extends Component {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteAlbumConfirmed}
|
||||
>
|
||||
{translate('Delete')}
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
@ -119,10 +119,7 @@
|
|||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -20,8 +19,6 @@ interface CssExports {
|
|||
'monitorToggleButton': string;
|
||||
'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,
|
||||
|
@ -218,8 +216,8 @@ class AlbumDetails extends Component {
|
|||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
trackFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
|
@ -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
|
||||
|
@ -418,18 +415,18 @@ class AlbumDetails extends Component {
|
|||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
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.sizeOnDisk}>
|
||||
{
|
||||
moment(releaseDate).format(shortDateFormat)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
|
@ -438,15 +435,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 +460,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 ? 'Monitored' : '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 +494,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}>
|
||||
Links
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -553,9 +527,8 @@ class AlbumDetails extends Component {
|
|||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
!isPopulated && !albumsError && !trackFilesError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -566,14 +539,6 @@ class AlbumDetails extends Component {
|
|||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && tracksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('TracksLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && trackFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
|
@ -602,14 +567,6 @@ class AlbumDetails extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !media.length ?
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('NoMediumInformation')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
|
@ -676,7 +633,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 +659,6 @@ AlbumDetails.propTypes = {
|
|||
};
|
||||
|
||||
AlbumDetails.defaultProps = {
|
||||
secondaryTypes: [],
|
||||
statistics: {},
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
|
|
|
@ -70,12 +70,6 @@ function createMapStateToProps() {
|
|||
isCommandExecuting(isSearchingCommand) &&
|
||||
isSearchingCommand.body.albumIds.indexOf(album.id) > -1
|
||||
);
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
|
||||
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
||||
const isRenamingArtist = (
|
||||
isCommandExecuting(isRenamingArtistCommand) &&
|
||||
isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1
|
||||
);
|
||||
|
||||
const isFetching = tracks.isFetching || isTrackFilesFetching;
|
||||
const isPopulated = tracks.isPopulated && isTrackFilesPopulated;
|
||||
|
@ -86,8 +80,6 @@ function createMapStateToProps() {
|
|||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
artist,
|
||||
isSearching,
|
||||
isRenamingFiles,
|
||||
isRenamingArtist,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
tracksError,
|
||||
|
@ -121,27 +113,8 @@ class AlbumDetailsConnector extends Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
anyReleaseOk,
|
||||
isRenamingFiles,
|
||||
isRenamingArtist
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||
(prevProps.isRenamingArtist && !isRenamingArtist) ||
|
||||
!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && anyReleaseOk === true)
|
||||
) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
|
||||
// If the id has changed we need to clear the album
|
||||
// files and fetch from the server.
|
||||
|
||||
if (prevProps.id !== id) {
|
||||
if (!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
|
@ -201,8 +174,6 @@ class AlbumDetailsConnector extends Component {
|
|||
AlbumDetailsConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
anyReleaseOk: PropTypes.bool,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingArtist: PropTypes.bool.isRequired,
|
||||
isAlbumFetching: PropTypes.bool,
|
||||
isAlbumPopulated: PropTypes.bool,
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
|
|
|
@ -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,
|
||||
|
@ -43,10 +43,6 @@ class EditAlbumModalContent extends Component {
|
|||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
anyReleaseOk,
|
||||
|
@ -100,7 +96,7 @@ class EditAlbumModalContent extends Component {
|
|||
type={inputTypes.ALBUM_RELEASE_SELECT}
|
||||
name="releases"
|
||||
helpText={translate('ReleasesHelpText')}
|
||||
isDisabled={anyReleaseOk.value && trackFileCount > 0}
|
||||
isDisabled={anyReleaseOk.value && statistics.trackFileCount > 0}
|
||||
albumReleases={releases}
|
||||
onChange={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>
|
||||
|
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function getTooltip(title, quality, size) {
|
||||
if (!title) {
|
||||
|
@ -27,44 +26,13 @@ function getTooltip(title, quality, size) {
|
|||
return title;
|
||||
}
|
||||
|
||||
function revisionLabel(className, quality, showRevision) {
|
||||
if (!showRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (quality.revision.isRepack) {
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kinds.PRIMARY}
|
||||
title={translate('Repack')}
|
||||
>
|
||||
R
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
if (quality.revision.version && quality.revision.version > 1) {
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kinds.PRIMARY}
|
||||
title={translate('Proper')}
|
||||
>
|
||||
P
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TrackQuality(props) {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
quality,
|
||||
size,
|
||||
isCutoffNotMet,
|
||||
showRevision
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
|
||||
if (!quality) {
|
||||
|
@ -72,15 +40,13 @@ function TrackQuality(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Label
|
||||
className={className}
|
||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||
title={getTooltip(title, quality, size)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>{revisionLabel(className, quality, showRevision)}
|
||||
</span>
|
||||
<Label
|
||||
className={className}
|
||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||
title={getTooltip(title, quality, size)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -89,13 +55,11 @@ TrackQuality.propTypes = {
|
|||
title: PropTypes.string,
|
||||
quality: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
isCutoffNotMet: PropTypes.bool,
|
||||
showRevision: PropTypes.bool
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
};
|
||||
|
||||
TrackQuality.defaultProps = {
|
||||
title: '',
|
||||
showRevision: false
|
||||
title: ''
|
||||
};
|
||||
|
||||
export default TrackQuality;
|
||||
|
|
|
@ -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';
|
||||
|
@ -43,30 +39,16 @@ export interface CustomFilter {
|
|||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
version: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
albums: AlbumAppState;
|
||||
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,10 +1,38 @@
|
|||
import Queue from 'typings/Queue';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
AppSectionItemState,
|
||||
Error,
|
||||
} from './AppSectionState';
|
||||
|
||||
export interface StatusMessage {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
export interface Queue extends ModelBase {
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
statusMessages: StatusMessage[];
|
||||
errorMessage: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
outputPath: string;
|
||||
artistId?: number;
|
||||
albumId?: number;
|
||||
}
|
||||
|
||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
|
|
@ -1,28 +1,21 @@
|
|||
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 +38,16 @@ 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;
|
||||
|
|
|
@ -56,8 +56,8 @@ class DeleteArtistModalContent extends Component {
|
|||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
trackFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -86,10 +85,9 @@ class AlbumRow extends Component {
|
|||
} = this.props;
|
||||
|
||||
const {
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
totalTrackCount = 0,
|
||||
sizeOnDisk = 0
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount
|
||||
} = 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
|
||||
|
@ -268,8 +257,7 @@ AlbumRow.propTypes = {
|
|||
AlbumRow.defaultProps = {
|
||||
statistics: {
|
||||
trackCount: 0,
|
||||
trackFileCount: 0,
|
||||
totalTrackCount: 0
|
||||
trackFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -192,7 +192,7 @@ class ArtistDetails extends Component {
|
|||
artistName,
|
||||
ratings,
|
||||
path,
|
||||
statistics = {},
|
||||
statistics,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
genres,
|
||||
|
@ -222,8 +222,8 @@ class ArtistDetails extends Component {
|
|||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
trackFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
|
@ -242,7 +242,7 @@ class ArtistDetails extends Component {
|
|||
} = this.state;
|
||||
|
||||
const continuing = status === 'continuing';
|
||||
const endedString = artistType === 'Person' ? translate('Deceased') : translate('Inactive');
|
||||
const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive';
|
||||
|
||||
let trackFilesCountMessage = translate('TrackFilesCountMessage');
|
||||
|
||||
|
@ -556,7 +556,7 @@ class ArtistDetails extends Component {
|
|||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
Links
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ function createMapStateToProps() {
|
|||
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
|
||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id }));
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
|
||||
|
||||
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
||||
const isRenamingArtist = (
|
||||
isCommandExecuting(isRenamingArtistCommand) &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector';
|
||||
|
||||
function ArtistHistoryModal(props) {
|
||||
|
@ -14,7 +13,6 @@ function ArtistHistoryModal(props) {
|
|||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ArtistHistoryModalContentConnector
|
||||
|
|
|
@ -35,9 +35,13 @@ const columns = [
|
|||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
label: () => translate('Details'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
@ -49,13 +53,9 @@ const columns = [
|
|||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: () => translate('Actions'),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.details,
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'details': string;
|
||||
'sourceTitle': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
|
|
@ -11,6 +11,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||
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 formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
@ -111,19 +112,11 @@ class ArtistHistoryRow extends Component {
|
|||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<AlbumFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={date}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<TableRowCell className={styles.details}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
|
@ -141,13 +134,25 @@ class ArtistHistoryRow extends Component {
|
|||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<AlbumFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } 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';
|
||||
|
@ -30,7 +22,6 @@ import {
|
|||
setArtistView,
|
||||
} from 'Store/Actions/artistIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
|
@ -57,7 +48,7 @@ import ArtistIndexTable from './Table/ArtistIndexTable';
|
|||
import ArtistIndexTableOptions from './Table/ArtistIndexTableOptions';
|
||||
import styles from './ArtistIndex.css';
|
||||
|
||||
function getViewComponent(view: string) {
|
||||
function getViewComponent(view) {
|
||||
if (view === 'posters') {
|
||||
return ArtistIndexPosters;
|
||||
}
|
||||
|
@ -91,25 +82,18 @@ 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(() => {
|
||||
dispatch(fetchQueueDetails({ all: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
const onRssSyncPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
|
@ -123,14 +107,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 +125,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 +147,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 +169,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 +289,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}
|
||||
|
|
|
@ -50,12 +50,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.downloading {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: var(--purple);
|
||||
}
|
||||
|
||||
.statistics {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'continuing': string;
|
||||
'downloading': string;
|
||||
'ended': string;
|
||||
'footer': string;
|
||||
'legendItem': string;
|
||||
|
|
|
@ -113,16 +113,6 @@ export default function ArtistIndexFooter() {
|
|||
/>
|
||||
<div>{translate('MissingTracksArtistNotMonitored')}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.downloading,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>{translate('ArtistIndexFooterDownloading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statistics}>
|
||||
|
|
|
@ -183,15 +183,13 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
|
|||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
artistId={artistId}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
width={bannerWidth}
|
||||
posterWidth={bannerWidth}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
isStandalone={false}
|
||||
/>
|
||||
|
||||
{showTitle ? (
|
||||
|
@ -206,7 +204,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;
|
||||
|
|
|
@ -160,15 +160,13 @@ function ArtistIndexOverview(props: ArtistIndexOverviewProps) {
|
|||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
artistId={artistId}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
width={posterWidth}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={overviewOptions.detailedProgressBar}
|
||||
isStandalone={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -183,15 +183,13 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
|
|||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
artistId={artistId}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
width={posterWidth}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
isStandalone={false}
|
||||
/>
|
||||
|
||||
{showTitle ? (
|
||||
|
@ -206,7 +204,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 });
|
||||
|
||||
|
@ -202,15 +201,11 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
|
||||
if (isSmallScreen) {
|
||||
const padding = bodyPaddingSmallScreen - 5;
|
||||
const width = window.innerWidth - padding * 2;
|
||||
const height = window.innerHeight;
|
||||
|
||||
if (width !== size.width || height !== size.height) {
|
||||
setSize({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
setSize({
|
||||
width: window.innerWidth - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -218,22 +213,17 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
if (current) {
|
||||
const width = current.clientWidth;
|
||||
const padding = bodyPadding - 5;
|
||||
const finalWidth = width - padding * 2;
|
||||
|
||||
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSize({
|
||||
width: finalWidth,
|
||||
width: width - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||
}, [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;
|
||||
|
@ -242,7 +232,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 +255,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,8 +1,4 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createArtistQueueItemsDetailsSelector, {
|
||||
ArtistQueueDetails,
|
||||
} from 'Artist/Index/createArtistQueueDetailsSelector';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
|
||||
|
@ -10,51 +6,35 @@ import translate from 'Utilities/String/translate';
|
|||
import styles from './ArtistIndexProgressBar.css';
|
||||
|
||||
interface ArtistIndexProgressBarProps {
|
||||
artistId: number;
|
||||
monitored: boolean;
|
||||
status: string;
|
||||
trackCount: number;
|
||||
trackFileCount: number;
|
||||
totalTrackCount: number;
|
||||
width: number;
|
||||
posterWidth: number;
|
||||
detailedProgressBar: boolean;
|
||||
isStandalone: boolean;
|
||||
}
|
||||
|
||||
function ArtistIndexProgressBar(props: ArtistIndexProgressBarProps) {
|
||||
const {
|
||||
artistId,
|
||||
monitored,
|
||||
status,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount,
|
||||
width,
|
||||
posterWidth,
|
||||
detailedProgressBar,
|
||||
isStandalone,
|
||||
} = props;
|
||||
|
||||
const queueDetails: ArtistQueueDetails = useSelector(
|
||||
createArtistQueueItemsDetailsSelector(artistId)
|
||||
);
|
||||
|
||||
const newDownloads = queueDetails.count - queueDetails.tracksWithFiles;
|
||||
const progress = trackCount ? (trackFileCount / trackCount) * 100 : 100;
|
||||
const text = newDownloads
|
||||
? `${trackFileCount} + ${newDownloads} / ${trackCount}`
|
||||
: `${trackFileCount} / ${trackCount}`;
|
||||
const text = `${trackFileCount} / ${trackCount}`;
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
className={styles.progressBar}
|
||||
containerClassName={isStandalone ? undefined : styles.progress}
|
||||
containerClassName={styles.progress}
|
||||
progress={progress}
|
||||
kind={getProgressBarKind(
|
||||
status,
|
||||
monitored,
|
||||
progress,
|
||||
queueDetails.count > 0
|
||||
)}
|
||||
kind={getProgressBarKind(status, monitored, progress)}
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
showText={detailedProgressBar}
|
||||
text={text}
|
||||
|
@ -62,9 +42,8 @@ function ArtistIndexProgressBar(props: ArtistIndexProgressBarProps) {
|
|||
trackFileCount,
|
||||
trackCount,
|
||||
totalTrackCount,
|
||||
downloadingCount: queueDetails.count,
|
||||
})}
|
||||
width={width}
|
||||
width={posterWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
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