mirror of https://github.com/lidarr/Lidarr
Compare commits
160 Commits
v2.1.6.399
...
develop
Author | SHA1 | Date |
---|---|---|
Bogdan | 959f6be019 | |
Servarr | ceca76d7c0 | |
Weblate | 3b0f9500a8 | |
Bogdan | 9eda077c03 | |
Bogdan | 4832860cce | |
Mark McDowall | 448d29f135 | |
Bogdan | 7d46360c34 | |
Bogdan | 4752b54e26 | |
Weblate | 17bf73d1ef | |
Bogdan | 6ec298ed2a | |
Bogdan | 563db9231e | |
Bogdan | d27b062d6a | |
Bogdan | febb3ef485 | |
sillock1 | 30d9891bf0 | |
Bogdan | 2621acdae5 | |
Bogdan | 18772553f2 | |
Weblate | 0300bf2dd2 | |
Bogdan | 77861e4303 | |
Bogdan | 3545a7451e | |
Mark McDowall | 9f8c4530ca | |
Mickaël Thomas | 9da690f807 | |
Bogdan | 31f342b8ad | |
Bogdan | 596a36d45f | |
Bogdan | 94bb8a436b | |
Weblate | 94d2a20b6a | |
Bogdan | a25e5aae10 | |
Mark McDowall | f4a02ffc83 | |
Bogdan | 1bdcf91014 | |
Bogdan | 4d28d3f25a | |
Bogdan | 9660ec37cd | |
Bogdan | 66c7521f4b | |
Stevie Robinson | 8b57b33c99 | |
Mark McDowall | 580e4becbe | |
Christopher | 5f248aa25e | |
Bogdan | a735eccb65 | |
Weblate | d11ed42830 | |
Bogdan | b0038dd143 | |
Bogdan | 2e242aeb7b | |
Bogdan | 416d505316 | |
Bogdan | 4816f35256 | |
Josh McKinney | e42e0a72eb | |
Bogdan | db9e62f79d | |
Bogdan | bc69fa4842 | |
Bogdan | 86dad72c49 | |
Bogdan | 4a8d6c367d | |
Bogdan | c1926f8758 | |
Bogdan | 7820bcf91f | |
Servarr | 431ad0a028 | |
Bogdan | 59cf7a95c3 | |
Bogdan | e17e3633f8 | |
Bogdan | 46da2b49c6 | |
Josh McKinney | 3071977284 | |
Mark McDowall | b14e2bb618 | |
Mark McDowall | 8c09c0cb5c | |
Weblate | 8cebb21c2d | |
Bogdan | 74ac263b74 | |
Bogdan | adcec90ef8 | |
Qstick | daf8b94c8e | |
Bogdan | 7c4f0c597e | |
Bogdan | 1d2af2aab4 | |
Stevie Robinson | 5d537689fb | |
Cuki | ca6beea62b | |
Mark McDowall | a82c919093 | |
Mark McDowall | 2941e0c4b7 | |
Bogdan | ca0b900d92 | |
Weblate | 72f1b2075b | |
Bogdan | e847828191 | |
Bogdan | 2a10505dff | |
Mark McDowall | 28f2eb974d | |
Servarr | 13ce040e4d | |
Weblate | f477f9b287 | |
Bogdan | 0e84008669 | |
Louis R | 52b5ff6fdd | |
Carlos Gustavo Sarmiento | 1d0de51917 | |
Stevie Robinson | a8648fdb71 | |
Bogdan | f890a8c18f | |
Mark McDowall | e730cf6307 | |
Bogdan | 9f4d821a2d | |
Weblate | ce6e4555ec | |
Mark McDowall | 55eaecb3c8 | |
Bogdan | 63e36f71d2 | |
Bogdan | 89e184e768 | |
Mark McDowall | 873a225f0c | |
Stevie Robinson | b81170d911 | |
Servarr | 5ffde40320 | |
Mark McDowall | ebfa68087d | |
Bogdan | 1db0eb1029 | |
Bogdan | 967b58017a | |
Bogdan | 3754d611c7 | |
Weblate | 8035d4202f | |
Bogdan | 468f3acf85 | |
Bogdan | 29c77ec3a1 | |
bpoxy | d04bb5333a | |
Bogdan | 0d76fbcf0d | |
Weblate | 3df140b1f0 | |
Bogdan | 340ae78f46 | |
Helvio Pedreschi | 881fabad93 | |
Mark McDowall | be8f7e5618 | |
Bogdan | c9743448fd | |
Bogdan | 47e647ddb1 | |
Mark McDowall | f6529d5ad3 | |
Mark McDowall | fb1b7274d0 | |
Louis R | 33b12a532c | |
nopoz | cea5ee503f | |
Bogdan | 475590a21b | |
Weblate | 0ca0f68af1 | |
Bogdan | 2c19b5aa61 | |
Weblate | 7e0c5e0da5 | |
Mark McDowall | adecb7f73c | |
Mark McDowall | 98a90e2f8f | |
Bogdan | ce2bb5be1f | |
Bogdan | e446c25a01 | |
servarr[bot] | d38c101acd | |
Bogdan | 022fbf864c | |
Bogdan | 3ff9b8bd85 | |
Bogdan | 57926a61d2 | |
bakerboy448 | 87f88af7ee | |
Weblate | 30fc3fc70a | |
Bogdan | 4abca0c896 | |
Bogdan | b2f595436b | |
Bogdan | e7ae0b9e22 | |
Bogdan | 0431b257e1 | |
abcasada | 479e8cce20 | |
Bogdan | 27723eb3ea | |
Qstick | 616b529c9a | |
Qstick | 8b85d4c941 | |
Servarr | f13b095040 | |
Qstick | a4af75b60c | |
Qstick | c7faf7cc25 | |
Weblate | 7f0fab0cf6 | |
Bogdan | d68f207e9b | |
Bogdan | f1efd05207 | |
Weblate | 59efffd40f | |
Mark McDowall | 6c90ac74e9 | |
Mark McDowall | f5eee52194 | |
Mark McDowall | 0871949b74 | |
Bogdan | 1536e90053 | |
Bogdan | c744231141 | |
Mark McDowall | efe0a3d283 | |
Qstick | 8e5942d5c5 | |
Weblate | 6471353bcd | |
Qstick | c3c50498bd | |
Bogdan | 6ae99acea7 | |
Bogdan | d8066ec172 | |
Mark McDowall | a9b16d298f | |
Mark McDowall | 0bdd5f3278 | |
Mark McDowall | 2f0d02b3bc | |
Bogdan | abefdca0fc | |
Bogdan | 2fc966af0c | |
Mark McDowall | 5d8f9c9e27 | |
Mark McDowall | 0bcbf9df81 | |
Bogdan | 49883d0e30 | |
Servarr | 09e9162aa6 | |
Qstick | d38c44d25e | |
Qstick | 3702fa773c | |
Qstick | aecf5bba49 | |
Qstick | 6e43d8a4fe | |
Qstick | 2a8c67badc | |
Stevie Robinson | 0121095b3e | |
Bogdan | 2f80957f11 |
|
@ -0,0 +1,13 @@
|
|||
// 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": {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// 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": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8686],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# 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
|
|
@ -126,6 +126,7 @@ coverage*.xml
|
|||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,14 +9,14 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.1.6'
|
||||
majorVersion: '2.4.0'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '16.X'
|
||||
dotnetVersion: '6.0.421'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
|
@ -166,10 +166,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
@ -1093,10 +1093,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||
|
||||
# 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 doesnt 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
|
|
@ -0,0 +1,20 @@
|
|||
# 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
|
10
docs.sh
10
docs.sh
|
@ -21,15 +21,21 @@ 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.5.0 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/lidarr.console.dll" v1 &
|
||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v1 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ module.exports = {
|
|||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false
|
||||
sinon: false,
|
||||
JSX: true
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
|
|
@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
title,
|
||||
statistics,
|
||||
statistics = {},
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ class AlbumDetailsMedium extends Component {
|
|||
</Table> :
|
||||
|
||||
<div className={styles.noTracks}>
|
||||
No tracks in this medium
|
||||
{translate('NoTracksInThisMedium')}
|
||||
</div>
|
||||
}
|
||||
<div className={styles.collapseButtonContainer}>
|
||||
|
|
|
@ -35,3 +35,9 @@
|
|||
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.indexerFlags {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ interface CssExports {
|
|||
'audio': string;
|
||||
'customFormatScore': string;
|
||||
'duration': string;
|
||||
'indexerFlags': string;
|
||||
'monitored': string;
|
||||
'size': string;
|
||||
'status': string;
|
||||
|
|
|
@ -2,15 +2,19 @@ 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 { tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds, 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';
|
||||
|
||||
|
@ -32,6 +36,7 @@ class TrackRow extends Component {
|
|||
trackFileSize,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
indexerFlags,
|
||||
columns,
|
||||
deleteTrackFile
|
||||
} = this.props;
|
||||
|
@ -141,12 +146,30 @@ class TrackRow extends Component {
|
|||
customFormats.length
|
||||
)}
|
||||
tooltip={<AlbumFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</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
|
||||
|
@ -208,12 +231,14 @@ 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: []
|
||||
customFormats: [],
|
||||
indexerFlags: 0
|
||||
};
|
||||
|
||||
export default TrackRow;
|
||||
|
|
|
@ -13,7 +13,8 @@ function createMapStateToProps() {
|
|||
trackFilePath: trackFile ? trackFile.path : null,
|
||||
trackFileSize: trackFile ? trackFile.size : null,
|
||||
customFormats: trackFile ? trackFile.customFormats : [],
|
||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0
|
||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0,
|
||||
indexerFlags: trackFile ? trackFile.indexerFlags : 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
|
|||
title,
|
||||
artistName,
|
||||
albumType,
|
||||
statistics,
|
||||
statistics = {},
|
||||
item,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
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_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -7,6 +7,7 @@ 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 {
|
||||
|
@ -18,7 +19,10 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||
{albumTitle === undefined ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
|
@ -32,7 +36,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
@ -12,11 +12,10 @@ function App({ store, history }) {
|
|||
<DocumentTitle title={window.Lidarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
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);
|
|
@ -0,0 +1,37 @@
|
|||
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,8 +1,11 @@
|
|||
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';
|
||||
|
@ -52,12 +55,15 @@ interface AppState {
|
|||
app: AppSectionState;
|
||||
artist: ArtistAppState;
|
||||
artistIndex: ArtistIndexAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
queue: QueueAppState;
|
||||
settings: SettingsAppState;
|
||||
tags: TagsAppState;
|
||||
trackFiles: TrackFilesAppState;
|
||||
tracksSelection: TracksAppState;
|
||||
system: SystemAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import Album from 'Album/Album';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
interface CalendarAppState
|
||||
extends AppSectionState<Album>,
|
||||
AppSectionFilterState<Album> {}
|
||||
|
||||
export default CalendarAppState;
|
|
@ -0,0 +1,6 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
|
@ -1,11 +1,13 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
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';
|
||||
|
@ -44,17 +46,19 @@ export interface RootFolderAppState
|
|||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
metadataProfiles: MetadataProfilesAppState;
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
|
||||
interface SystemAppState {
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
|
@ -1,12 +1,32 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
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;
|
||||
}
|
||||
|
||||
export default TagsAppState;
|
||||
|
|
|
@ -36,6 +36,7 @@ interface Artist extends ModelBase {
|
|||
nextAlbum?: Album;
|
||||
qualityProfileId: number;
|
||||
metadataProfileId: number;
|
||||
monitorNewItems: string;
|
||||
ratings: Ratings;
|
||||
rootFolderPath: string;
|
||||
sortName: string;
|
||||
|
|
|
@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteArtistConfirmed}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
@ -161,9 +161,7 @@ DeleteArtistModalContent.propTypes = {
|
|||
};
|
||||
|
||||
DeleteArtistModalContent.defaultProps = {
|
||||
statistics: {
|
||||
trackFileCount: 0
|
||||
}
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default DeleteArtistModalContent;
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
width: 42px;
|
||||
}
|
||||
|
||||
.size,
|
||||
.status {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'monitored': string;
|
||||
'size': string;
|
||||
'status': string;
|
||||
'title': string;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ 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';
|
||||
|
||||
|
@ -87,7 +88,8 @@ class AlbumRow extends Component {
|
|||
const {
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
totalTrackCount = 0
|
||||
totalTrackCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
return (
|
||||
|
@ -196,6 +198,17 @@ class AlbumRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.size}
|
||||
>
|
||||
{!!sizeOnDisk && formatBytes(sizeOnDisk)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
|
|
@ -192,7 +192,7 @@ class ArtistDetails extends Component {
|
|||
artistName,
|
||||
ratings,
|
||||
path,
|
||||
statistics,
|
||||
statistics = {},
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
genres,
|
||||
|
|
|
@ -196,7 +196,7 @@ class ArtistDetailsSeason extends Component {
|
|||
trackFileCount,
|
||||
monitoredAlbumCount,
|
||||
hasMonitoredAlbums,
|
||||
sizeOnDisk
|
||||
sizeOnDisk = 0
|
||||
} = getAlbumStatistics(items);
|
||||
|
||||
const {
|
||||
|
|
|
@ -7,6 +7,8 @@ import React, {
|
|||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import { RSS_SYNC } from 'Commands/commandNames';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
@ -89,16 +91,19 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
} = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
|
||||
}: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState =
|
||||
useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
|
||||
|
||||
const isRssSyncExecuting = useSelector(
|
||||
createCommandExecutingSelector(RSS_SYNC)
|
||||
);
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
const dispatch = useDispatch();
|
||||
const scrollerRef = useRef<HTMLDivElement>();
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -118,14 +123,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
}, [isSelectMode, setIsSelectMode]);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onViewSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setArtistView({ view: value }));
|
||||
|
||||
if (scrollerRef.current) {
|
||||
|
@ -136,14 +141,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
);
|
||||
|
||||
const onSortSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setArtistSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setArtistFilter({ selectedFilterKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -158,15 +163,15 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
}, [setIsOptionsModalOpen]);
|
||||
|
||||
const onJumpBarItemPress = useCallback(
|
||||
(character) => {
|
||||
(character: string) => {
|
||||
setJumpToCharacter(character);
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
({ scrollTop }) => {
|
||||
setJumpToCharacter(null);
|
||||
({ scrollTop }: { scrollTop: number }) => {
|
||||
setJumpToCharacter(undefined);
|
||||
scrollPositions.artistIndex = scrollTop;
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
|
@ -180,10 +185,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
|||
};
|
||||
}
|
||||
|
||||
const characters = items.reduce((acc, item) => {
|
||||
const characters = items.reduce((acc: Record<string, number>, item) => {
|
||||
let char = item.sortName.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
if (!isNaN(Number(char))) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
|
@ -300,6 +305,8 @@ 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,7 +23,13 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
export default function ArtistIndexFilterModal(props) {
|
||||
interface ArtistIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function ArtistIndexFilterModal(
|
||||
props: ArtistIndexFilterModalProps
|
||||
) {
|
||||
const sectionItems = useSelector(createArtistSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'artist';
|
||||
|
@ -31,7 +37,7 @@ export default function ArtistIndexFilterModal(props) {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setArtistFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -39,6 +45,7 @@ export default function ArtistIndexFilterModal(props) {
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
|
|
@ -206,7 +206,7 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{showQualityProfile ? (
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, 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';
|
||||
|
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
|
|||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const ADDITIONAL_COLUMN_COUNT = {
|
||||
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1,
|
||||
|
@ -41,17 +42,17 @@ interface CellItemData {
|
|||
|
||||
interface ArtistIndexBannersProps {
|
||||
items: Artist[];
|
||||
sortKey?: string;
|
||||
sortKey: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const artistIndexSelector = createSelector(
|
||||
(state) => state.artistIndex.bannerOptions,
|
||||
(state: AppState) => state.artistIndex.bannerOptions,
|
||||
(bannerOptions) => {
|
||||
return {
|
||||
bannerOptions,
|
||||
|
@ -108,7 +109,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
|||
} = props;
|
||||
|
||||
const { bannerOptions } = useSelector(artistIndexSelector);
|
||||
const ref: React.MutableRefObject<Grid> = useRef();
|
||||
const ref = useRef<Grid>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
@ -222,8 +223,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
|||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -232,7 +233,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);
|
||||
|
@ -255,8 +256,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, value }: { name: string; value: unknown }) => {
|
||||
dispatch(setArtistBannerOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
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';
|
||||
|
||||
function ArtistIndexFilterMenu(props) {
|
||||
interface ArtistIndexFilterMenuProps {
|
||||
selectedFilterKey: string | number;
|
||||
filters: object[];
|
||||
customFilters: CustomFilter[];
|
||||
isDisabled: boolean;
|
||||
onFilterSelect(filterName: string): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
|
@ -26,15 +34,6 @@ function ArtistIndexFilterMenu(props) {
|
|||
);
|
||||
}
|
||||
|
||||
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,11 +1,19 @@
|
|||
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, sortDirections } from 'Helpers/Props';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function ArtistIndexSortMenu(props) {
|
||||
interface SeriesIndexSortMenuProps {
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
isDisabled: boolean;
|
||||
onSortSelect(sortKey: string): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
|
||||
|
||||
return (
|
||||
|
@ -17,7 +25,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Monitored/Status
|
||||
{translate('MonitoredStatus')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -26,7 +34,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Name
|
||||
{translate('Name')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -35,7 +43,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Type
|
||||
{translate('Type')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -44,7 +52,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Quality Profile
|
||||
{translate('QualityProfile')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -53,7 +61,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Metadata Profile
|
||||
{translate('MetadataProfile')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -62,7 +70,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Next Album
|
||||
{translate('NextAlbum')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -71,7 +79,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Last Album
|
||||
{translate('Last Album')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -80,7 +88,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Added
|
||||
{translate('Added')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -89,7 +97,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Albums
|
||||
{translate('Albums')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -98,7 +106,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Tracks
|
||||
{translate('Tracks')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -107,7 +115,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Track Count
|
||||
{translate('TrackCount')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -116,7 +124,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Path
|
||||
{translate('Path')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -125,7 +133,7 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Size on Disk
|
||||
{translate('SizeOnDisk')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
@ -134,18 +142,11 @@ function ArtistIndexSortMenu(props) {
|
|||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Tags
|
||||
{translate('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,4 +1,3 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import ViewMenu from 'Components/Menu/ViewMenu';
|
||||
|
@ -6,7 +5,13 @@ import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
|||
import { align } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function ArtistIndexViewMenu(props) {
|
||||
interface ArtistIndexViewMenuProps {
|
||||
view: string;
|
||||
isDisabled: boolean;
|
||||
onViewSelect(value: string): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) {
|
||||
const { view, isDisabled, onViewSelect } = props;
|
||||
|
||||
return (
|
||||
|
@ -36,10 +41,4 @@ function ArtistIndexViewMenu(props) {
|
|||
);
|
||||
}
|
||||
|
||||
ArtistIndexViewMenu.propTypes = {
|
||||
view: PropTypes.string.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ArtistIndexViewMenu;
|
||||
|
|
|
@ -1,15 +1,51 @@
|
|||
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/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 = [
|
||||
|
@ -50,11 +86,17 @@ const rows = [
|
|||
},
|
||||
];
|
||||
|
||||
function getInfoRowProps(row, props, uiSettings) {
|
||||
function getInfoRowProps(
|
||||
row: RowProps,
|
||||
props: ArtistIndexOverviewInfoProps,
|
||||
uiSettings: UiSettings
|
||||
): RowInfoProps | null {
|
||||
const { name } = row;
|
||||
|
||||
if (name === 'monitored') {
|
||||
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
||||
const monitoredText = props.monitored
|
||||
? translate('Monitored')
|
||||
: translate('Unmonitored');
|
||||
|
||||
return {
|
||||
title: monitoredText,
|
||||
|
@ -63,9 +105,9 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
title: translate('QualityProfile'),
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name,
|
||||
};
|
||||
|
@ -78,15 +120,16 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
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,
|
||||
}
|
||||
) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -98,10 +141,11 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
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,
|
||||
}) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -116,7 +160,7 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
}
|
||||
|
||||
return {
|
||||
title: 'Album Count',
|
||||
title: translate('AlbumCount'),
|
||||
iconName: icons.CIRCLE,
|
||||
label: albums,
|
||||
};
|
||||
|
@ -124,7 +168,7 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
|
||||
if (name === 'path') {
|
||||
return {
|
||||
title: 'Path',
|
||||
title: translate('Path'),
|
||||
iconName: icons.FOLDER,
|
||||
label: props.path,
|
||||
};
|
||||
|
@ -132,31 +176,13 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return {
|
||||
title: 'Size on Disk',
|
||||
title: translate('SizeOnDisk'),
|
||||
iconName: icons.DRIVE,
|
||||
label: formatBytes(props.sizeOnDisk),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
||||
|
@ -175,6 +201,8 @@ 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 {
|
||||
|
@ -219,6 +247,10 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
|||
|
||||
const infoRowProps = getInfoRowProps(row, props, uiSettings);
|
||||
|
||||
if (infoRowProps == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
|
||||
})}
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
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: object;
|
||||
label: string;
|
||||
iconName?: IconDefinition;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, 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: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
|||
const { size: posterSize, detailedProgressBar } = useSelector(
|
||||
selectOverviewOptions
|
||||
);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const listRef = useRef<List>(null);
|
||||
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 currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
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, value }: { name: string; value: unknown }) => {
|
||||
dispatch(setArtistOverviewOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{showQualityProfile ? (
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, 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';
|
||||
|
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
|
|||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const ADDITIONAL_COLUMN_COUNT = {
|
||||
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1,
|
||||
|
@ -41,17 +42,17 @@ interface CellItemData {
|
|||
|
||||
interface ArtistIndexPostersProps {
|
||||
items: Artist[];
|
||||
sortKey?: string;
|
||||
sortKey: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const artistIndexSelector = createSelector(
|
||||
(state) => state.artistIndex.posterOptions,
|
||||
(state: AppState) => state.artistIndex.posterOptions,
|
||||
(posterOptions) => {
|
||||
return {
|
||||
posterOptions,
|
||||
|
@ -108,7 +109,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
} = props;
|
||||
|
||||
const { posterOptions } = useSelector(artistIndexSelector);
|
||||
const ref: React.MutableRefObject<Grid> = useRef();
|
||||
const ref = useRef<Grid>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
@ -231,8 +232,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
|||
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -241,7 +242,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);
|
||||
|
@ -264,8 +265,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, value }: { name: string; value: unknown }) => {
|
||||
dispatch(setArtistPosterOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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';
|
||||
|
@ -56,8 +57,8 @@ function AlbumDetails(props: AlbumDetailsProps) {
|
|||
disambiguation,
|
||||
albumType,
|
||||
monitored,
|
||||
statistics,
|
||||
isSaving,
|
||||
statistics = {} as Statistics,
|
||||
isSaving = false,
|
||||
} = album;
|
||||
|
||||
return (
|
||||
|
|
|
@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps {
|
|||
artistId: number;
|
||||
albumId: number;
|
||||
title: string;
|
||||
disambiguation: string;
|
||||
disambiguation?: string;
|
||||
albumType: string;
|
||||
monitored: boolean;
|
||||
statistics: Statistics;
|
||||
|
|
|
@ -33,7 +33,7 @@ function ChangeMonitoringModalContent(
|
|||
const [monitor, setMonitor] = useState(NO_CHANGE);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setMonitor(value);
|
||||
},
|
||||
[setMonitor]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { SyntheticEvent, useCallback } from 'react';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
@ -15,8 +15,9 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
|
|||
const isSelected = selectState.selectedState[artistId];
|
||||
|
||||
const onSelectPress = useCallback(
|
||||
(event) => {
|
||||
const shiftKey = event.nativeEvent.shiftKey;
|
||||
(event: SyntheticEvent) => {
|
||||
const nativeEvent = event.nativeEvent as PointerEvent;
|
||||
const shiftKey = nativeEvent.shiftKey;
|
||||
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
|||
interface ArtistIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
}
|
||||
|
||||
function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {
|
||||
|
|
|
@ -24,6 +24,14 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal';
|
|||
import TagsModal from './Tags/TagsModal';
|
||||
import styles from './ArtistIndexSelectFooter.css';
|
||||
|
||||
interface SavePayload {
|
||||
monitored?: boolean;
|
||||
qualityProfileId?: number;
|
||||
metadataProfileId?: number;
|
||||
rootFolderPath?: string;
|
||||
moveFiles?: boolean;
|
||||
}
|
||||
|
||||
const artistEditorSelector = createSelector(
|
||||
(state: AppState) => state.artist,
|
||||
(artist) => {
|
||||
|
@ -79,7 +87,7 @@ function ArtistIndexSelectFooter() {
|
|||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onSavePress = useCallback(
|
||||
(payload) => {
|
||||
(payload: SavePayload) => {
|
||||
setIsSavingArtist(true);
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
|
@ -118,7 +126,7 @@ function ArtistIndexSelectFooter() {
|
|||
}, [setIsTagsModalOpen]);
|
||||
|
||||
const onApplyTagsPress = useCallback(
|
||||
(tags, applyTags) => {
|
||||
(tags: number[], applyTags: string) => {
|
||||
setIsSavingTags(true);
|
||||
setIsTagsModalOpen(false);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps {
|
|||
label: string;
|
||||
iconName: IconDefinition;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,9 +28,15 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const artistNames = useMemo(() => {
|
||||
const artists = artistIds.map((id) => {
|
||||
return allArtists.find((a) => a.id === id);
|
||||
});
|
||||
const artists = artistIds.reduce((acc: Artist[], id) => {
|
||||
const a = allArtists.find((a) => a.id === id);
|
||||
|
||||
if (a) {
|
||||
acc.push(a);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const sorted = orderBy(artists, ['sortName']);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
|||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteArtistModalContent.css';
|
||||
|
||||
|
@ -37,16 +38,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) {
|
|||
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const artists = useMemo(() => {
|
||||
const artists = artistIds.map((id) => {
|
||||
const artists = useMemo((): Artist[] => {
|
||||
const artistList = artistIds.map((id) => {
|
||||
return allArtists.find((a) => a.id === id);
|
||||
});
|
||||
}) as Artist[];
|
||||
|
||||
return orderBy(artists, ['sortName']);
|
||||
return orderBy(artistList, ['sortName']);
|
||||
}, [artistIds, allArtists]);
|
||||
|
||||
const onDeleteFilesChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setDeleteFiles(value);
|
||||
},
|
||||
[setDeleteFiles]
|
||||
|
|
|
@ -35,7 +35,7 @@ const monitoredOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
|
@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
|
|||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||
|
||||
const save = useCallback(
|
||||
(moveFiles) => {
|
||||
(moveFiles: boolean) => {
|
||||
let hasChanges = false;
|
||||
const payload: SavePayload = {};
|
||||
|
||||
|
@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
|
|||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: { name: string; value: string }) => {
|
||||
switch (name) {
|
||||
case 'monitored':
|
||||
setMonitored(value);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { uniq } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
|
@ -28,7 +29,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
|||
const { artistIds, onModalClose, onApplyTagsPress } = props;
|
||||
|
||||
const allArtists: Artist[] = useSelector(createAllArtistSelector());
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||
|
||||
const [tags, setTags] = useState<number[]>([]);
|
||||
const [applyTags, setApplyTags] = useState('add');
|
||||
|
@ -48,14 +49,14 @@ function TagsModalContent(props: TagsModalContentProps) {
|
|||
}, [artistIds, allArtists]);
|
||||
|
||||
const onTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: number[] }) => {
|
||||
setTags(value);
|
||||
},
|
||||
[setTags]
|
||||
);
|
||||
|
||||
const onApplyTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setApplyTags(value);
|
||||
},
|
||||
[setApplyTags]
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.monitorNewItems,
|
||||
.nextAlbum,
|
||||
.lastAlbum,
|
||||
.added,
|
||||
|
|
|
@ -14,6 +14,7 @@ interface CssExports {
|
|||
'lastAlbum': string;
|
||||
'link': string;
|
||||
'metadataProfileId': string;
|
||||
'monitorNewItems': string;
|
||||
'nextAlbum': string;
|
||||
'overlayTitle': string;
|
||||
'path': string;
|
||||
|
|
|
@ -23,7 +23,9 @@ import Column from 'Components/Table/Column';
|
|||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumsCell from './AlbumsCell';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
|
@ -56,6 +58,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
monitored,
|
||||
status,
|
||||
path,
|
||||
monitorNewItems,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
added,
|
||||
|
@ -126,7 +129,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }) => {
|
||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
id,
|
||||
|
@ -217,15 +220,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{qualityProfile.name}
|
||||
{qualityProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -233,7 +228,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{metadataProfile.name}
|
||||
{metadataProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'monitorNewItems') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{translate(firstCharToUpper(monitorNewItems))}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -252,7 +255,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
}
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
None
|
||||
{translate('None')}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -271,13 +274,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
}
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
None
|
||||
{translate('None')}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
|
@ -328,7 +333,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
if (name === 'path') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{path}
|
||||
<span title={path}>{path}</span>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
|
||||
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
|
||||
|
@ -30,17 +31,17 @@ interface RowItemData {
|
|||
|
||||
interface ArtistIndexTableProps {
|
||||
items: Artist[];
|
||||
sortKey?: string;
|
||||
sortKey: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const columnsSelector = createSelector(
|
||||
(state) => state.artistIndex.columns,
|
||||
(state: AppState) => state.artistIndex.columns,
|
||||
(columns) => columns
|
||||
);
|
||||
|
||||
|
@ -93,7 +94,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
|
||||
const columns = useSelector(columnsSelector);
|
||||
const { showBanners } = useSelector(selectTableOptions);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const listRef = useRef<List<RowItemData>>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
const windowWidth = window.innerWidth;
|
||||
|
@ -104,7 +105,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
}, [showBanners]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef.current as HTMLElement;
|
||||
const current = scrollerRef?.current as HTMLElement;
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSize({
|
||||
|
@ -128,8 +129,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -138,7 +139,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -167,8 +168,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
scrollerRef?.current?.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.monitorNewItems,
|
||||
.nextAlbum,
|
||||
.lastAlbum,
|
||||
.added,
|
||||
|
|
|
@ -11,6 +11,7 @@ interface CssExports {
|
|||
'lastAlbum': string;
|
||||
'latestAlbum': string;
|
||||
'metadataProfileId': string;
|
||||
'monitorNewItems': string;
|
||||
'nextAlbum': string;
|
||||
'path': string;
|
||||
'qualityProfileId': string;
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
setArtistSort,
|
||||
setArtistTableOption,
|
||||
} from 'Store/Actions/artistIndexActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import styles from './ArtistIndexTableHeader.css';
|
||||
|
||||
|
@ -32,21 +33,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
|||
const [selectState, selectDispatch] = useSelect();
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setArtistSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
selectDispatch({
|
||||
type: value ? 'selectAll' : 'unselectAll',
|
||||
});
|
||||
|
@ -94,6 +95,8 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
|||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
styles[name],
|
||||
name === 'sortName' && showBanners && styles.banner,
|
||||
name === 'sortName' &&
|
||||
|
|
|
@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
|||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
|
||||
|
@ -19,7 +20,7 @@ function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) {
|
|||
const { showBanners, showSearchAction } = tableOptions;
|
||||
|
||||
const onTableOptionChangeWrapper = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
onTableOptionChange({
|
||||
tableOptions: {
|
||||
...tableOptions,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import Artist from 'Artist/Artist';
|
||||
import Command from 'Commands/Command';
|
||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
||||
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
||||
|
@ -12,25 +13,21 @@ function createArtistIndexItemSelector(artistId: number) {
|
|||
createArtistQualityProfileSelector(artistId),
|
||||
createArtistMetadataProfileSelector(artistId),
|
||||
createExecutingCommandsSelector(),
|
||||
(artist: Artist, qualityProfile, metadataProfile, executingCommands) => {
|
||||
// If an artist is deleted this selector may fire before the parent
|
||||
// selectors, which will result in an undefined artist, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show an artist that has no information available.
|
||||
|
||||
if (!artist) {
|
||||
return {};
|
||||
}
|
||||
|
||||
(
|
||||
artist: Artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
executingCommands: Command[]
|
||||
) => {
|
||||
const isRefreshingArtist = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === REFRESH_ARTIST && command.body.artistId === artist.id
|
||||
command.name === REFRESH_ARTIST && command.body.artistId === artistId
|
||||
);
|
||||
});
|
||||
|
||||
const isSearchingArtist = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === ARTIST_SEARCH && command.body.artistId === artist.id
|
||||
command.name === ARTIST_SEARCH && command.body.artistId === artistId
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) {
|
|||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
|
||||
function createCalendarSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.items,
|
||||
(calendar) => {
|
||||
return calendar;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface CalendarFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||
const sectionItems = useSelector(createCalendarSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'calendar';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setCalendarFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -14,6 +14,7 @@ import { align, icons } from 'Helpers/Props';
|
|||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CalendarConnector from './CalendarConnector';
|
||||
import CalendarFilterModal from './CalendarFilterModal';
|
||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||
import LegendConnector from './Legend/LegendConnector';
|
||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||
|
@ -78,6 +79,7 @@ class CalendarPage extends Component {
|
|||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
hasArtist,
|
||||
artistError,
|
||||
artistIsFetching,
|
||||
|
@ -137,7 +139,8 @@ class CalendarPage extends Component {
|
|||
isDisabled={!hasArtist}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={CalendarFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
@ -204,6 +207,7 @@ class CalendarPage extends Component {
|
|||
CalendarPage.propTypes = {
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasArtist: PropTypes.bool.isRequired,
|
||||
artistError: PropTypes.object,
|
||||
artistIsFetching: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -6,6 +6,7 @@ import withCurrentPage from 'Components/withCurrentPage';
|
|||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
@ -59,6 +60,7 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
(state) => state.calendar.selectedFilterKey,
|
||||
(state) => state.calendar.filters,
|
||||
createCustomFiltersSelector('calendar'),
|
||||
createArtistCountSelector(),
|
||||
createUISettingsSelector(),
|
||||
createMissingAlbumIdsSelector(),
|
||||
|
@ -67,6 +69,7 @@ function createMapStateToProps() {
|
|||
(
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
artistCount,
|
||||
uiSettings,
|
||||
missingAlbumIds,
|
||||
|
@ -76,6 +79,7 @@ function createMapStateToProps() {
|
|||
return {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
hasArtist: !!artistCount.count,
|
||||
artistError: artistCount.error,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export interface CommandBody {
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
completionMessage: string;
|
||||
requiresDiskAccess: boolean;
|
||||
isExclusive: boolean;
|
||||
isLongRunning: boolean;
|
||||
name: string;
|
||||
lastExecutionTime: string;
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
artistId?: number;
|
||||
artistIds?: number[];
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
name: string;
|
||||
commandName: string;
|
||||
message: string;
|
||||
body: CommandBody;
|
||||
priority: string;
|
||||
status: string;
|
||||
result: string;
|
||||
queued: string;
|
||||
started: string;
|
||||
ended: string;
|
||||
duration: string;
|
||||
trigger: string;
|
||||
stateChangeTime: string;
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
lastExecutionTime: string;
|
||||
}
|
||||
|
||||
export default Command;
|
|
@ -1,3 +1,4 @@
|
|||
import { maxBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
|||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = customFilters[customFilters.length -1];
|
||||
const last = maxBy(customFilters, 'id');
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
|
@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
|||
this.setState({
|
||||
labelErrors: [
|
||||
{
|
||||
message: 'Label is required'
|
||||
message: translate('LabelIsRequired')
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -146,7 +147,7 @@ class FilterBuilderModalContent extends Component {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Custom Filter
|
||||
{translate('CustomFilter')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
@ -166,7 +167,9 @@ class FilterBuilderModalContent extends Component {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.label}>{translate('Filters')}</div>
|
||||
<div className={styles.label}>
|
||||
{translate('Filters')}
|
||||
</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{
|
||||
|
|
|
@ -11,6 +11,7 @@ import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
|||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
|
||||
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
|
@ -68,6 +69,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||
case filterBuilderValueTypes.METADATA_PROFILE:
|
||||
return MetadataProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
|
||||
return MonitorNewItemsFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.PROTOCOL:
|
||||
return ProtocolFilterBuilderRowValue;
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const options = [
|
||||
{
|
||||
id: 'all',
|
||||
get name() {
|
||||
return translate('AllAlbums');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'new',
|
||||
get name() {
|
||||
return translate('New');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'none',
|
||||
get name() {
|
||||
return translate('None');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function MonitorNewItemsFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
return <FilterBuilderRowValue tagList={options} {...props} />;
|
||||
}
|
||||
|
||||
export default MonitorNewItemsFilterBuilderRowValue;
|
|
@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
|||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the delete was successful.
|
||||
// Moving this check to a ancestor would be more accurate, but would have
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
|
||||
interface ArtistTagInputProps {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
onChange: ({
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function ArtistTagInput(props: ArtistTagInputProps) {
|
||||
const { value, onChange, ...otherProps } = props;
|
||||
const isArray = Array.isArray(value);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||
if (isArray) {
|
||||
onChange({ name, value: newValue });
|
||||
} else {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isArray, onChange]
|
||||
);
|
||||
|
||||
let finalValue: number[] = [];
|
||||
|
||||
if (isArray) {
|
||||
finalValue = value;
|
||||
} else if (value === 0) {
|
||||
finalValue = [];
|
||||
} else {
|
||||
finalValue = [value];
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||
<TagInputConnector
|
||||
{...otherProps}
|
||||
value={finalValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -25,7 +25,8 @@ function createMapStateToProps() {
|
|||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||
return {
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name
|
||||
value: downloadClient.name,
|
||||
hint: `(${downloadClient.id})`
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
|
|
|
@ -4,6 +4,7 @@ import Link from 'Components/Link/Link';
|
|||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
|
||||
import ArtistTagInput from './ArtistTagInput';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
|
@ -12,6 +13,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
|||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
||||
|
@ -83,6 +85,9 @@ function getComponent(type) {
|
|||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInput;
|
||||
|
||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||
return DownloadClientSelectInputConnector;
|
||||
|
||||
|
@ -95,6 +100,9 @@ function getComponent(type) {
|
|||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.ARTIST_TAG:
|
||||
return ArtistTagInput;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
|
@ -292,6 +300,7 @@ FormInputGroup.propTypes = {
|
|||
includeNoChangeDisabled: PropTypes.bool,
|
||||
includeNone: PropTypes.bool,
|
||||
selectedValueOptions: PropTypes.object,
|
||||
indexerFlags: PropTypes.number,
|
||||
pending: PropTypes.bool,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||
createSelector(
|
||||
(state: AppState) => state.settings.indexerFlags,
|
||||
(indexerFlags) => {
|
||||
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if ((selectedFlags & id) === id) {
|
||||
acc.push(id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const values = indexerFlags.items.map(({ id, name }) => ({
|
||||
key: id,
|
||||
value: name,
|
||||
}));
|
||||
|
||||
return {
|
||||
value,
|
||||
values,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface IndexerFlagsSelectInputProps {
|
||||
name: string;
|
||||
indexerFlags: number;
|
||||
onChange(payload: object): void;
|
||||
}
|
||||
|
||||
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
||||
const { indexerFlags, onChange } = props;
|
||||
|
||||
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
||||
|
||||
const onChangeWrapper = useCallback(
|
||||
({ name, value }: { name: string; value: number[] }) => {
|
||||
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
||||
|
||||
onChange({ name, value: indexerFlags });
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
value={value}
|
||||
values={values}
|
||||
onChange={onChangeWrapper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerFlagsSelectInput;
|
|
@ -38,7 +38,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ function MonitorAlbumsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ function MonitorAlbumsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
font-family: $passwordFamily;
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
|
@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type="password"
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
className: styles.input
|
||||
...TextInput.props
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
|
|
@ -9,7 +9,6 @@ import TableBody from 'Components/Table/TableBody';
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import styles from './PlaylistInput.css';
|
||||
|
@ -46,7 +45,17 @@ class PlaylistInput extends Component {
|
|||
onChange
|
||||
} = this.props;
|
||||
|
||||
const oldSelected = getSelectedIds(prevState.selectedState, { parseIds: false }).sort();
|
||||
const oldSelected = _.reduce(
|
||||
prevState.selectedState,
|
||||
(result, value, id) => {
|
||||
if (value) {
|
||||
result.push(id);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[]
|
||||
).sort();
|
||||
const newSelected = this.getSelectedIds().sort();
|
||||
|
||||
if (!_.isEqual(oldSelected, newSelected)) {
|
||||
|
@ -61,7 +70,17 @@ class PlaylistInput extends Component {
|
|||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
||||
return _.reduce(
|
||||
this.state.selectedState,
|
||||
(result, value, id) => {
|
||||
if (value) {
|
||||
result.push(id);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
|
|
|
@ -29,6 +29,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'artistTag':
|
||||
return inputTypes.ARTIST_TAG;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
case 'tagSelect':
|
||||
|
|
|
@ -26,7 +26,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ function SeriesTypeSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ function SeriesTypeSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ class TextTagInputConnector extends Component {
|
|||
render() {
|
||||
return (
|
||||
<TagInput
|
||||
delimiters={['Tab', 'Enter', ',']}
|
||||
tagList={[]}
|
||||
onTagAdd={this.onTagAdd}
|
||||
onTagDelete={this.onTagDelete}
|
||||
|
|
|
@ -63,6 +63,13 @@
|
|||
width: 1280px;
|
||||
}
|
||||
|
||||
|
||||
.extraExtraLarge {
|
||||
composes: modal;
|
||||
|
||||
width: 1600px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
.modal.extraLarge {
|
||||
width: 90%;
|
||||
|
@ -90,7 +97,8 @@
|
|||
.modal.small,
|
||||
.modal.medium,
|
||||
.modal.large,
|
||||
.modal.extraLarge {
|
||||
.modal.extraLarge,
|
||||
.modal.extraExtraLarge {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'extraExtraLarge': string;
|
||||
'extraLarge': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
|
|
|
@ -6,7 +6,14 @@ import { createSelector } from 'reselect';
|
|||
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
fetchImportLists,
|
||||
fetchIndexerFlags,
|
||||
fetchLanguages,
|
||||
fetchMetadataProfiles,
|
||||
fetchQualityProfiles,
|
||||
fetchUISettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
|
@ -44,6 +51,7 @@ const selectAppProps = createSelector(
|
|||
);
|
||||
|
||||
const selectIsPopulated = createSelector(
|
||||
(state) => state.artist.isPopulated,
|
||||
(state) => state.customFilters.isPopulated,
|
||||
(state) => state.tags.isPopulated,
|
||||
(state) => state.settings.ui.isPopulated,
|
||||
|
@ -51,9 +59,11 @@ const selectIsPopulated = createSelector(
|
|||
(state) => state.settings.qualityProfiles.isPopulated,
|
||||
(state) => state.settings.metadataProfiles.isPopulated,
|
||||
(state) => state.settings.importLists.isPopulated,
|
||||
(state) => state.settings.indexerFlags.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(state) => state.app.translations.isPopulated,
|
||||
(
|
||||
artistsIsPopulated,
|
||||
customFiltersIsPopulated,
|
||||
tagsIsPopulated,
|
||||
uiSettingsIsPopulated,
|
||||
|
@ -61,10 +71,12 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated,
|
||||
metadataProfilesIsPopulated,
|
||||
importListsIsPopulated,
|
||||
indexerFlagsIsPopulated,
|
||||
systemStatusIsPopulated,
|
||||
translationsIsPopulated
|
||||
) => {
|
||||
return (
|
||||
artistsIsPopulated &&
|
||||
customFiltersIsPopulated &&
|
||||
tagsIsPopulated &&
|
||||
uiSettingsIsPopulated &&
|
||||
|
@ -72,6 +84,7 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated &&
|
||||
metadataProfilesIsPopulated &&
|
||||
importListsIsPopulated &&
|
||||
indexerFlagsIsPopulated &&
|
||||
systemStatusIsPopulated &&
|
||||
translationsIsPopulated
|
||||
);
|
||||
|
@ -79,6 +92,7 @@ const selectIsPopulated = createSelector(
|
|||
);
|
||||
|
||||
const selectErrors = createSelector(
|
||||
(state) => state.artist.error,
|
||||
(state) => state.customFilters.error,
|
||||
(state) => state.tags.error,
|
||||
(state) => state.settings.ui.error,
|
||||
|
@ -86,9 +100,11 @@ const selectErrors = createSelector(
|
|||
(state) => state.settings.qualityProfiles.error,
|
||||
(state) => state.settings.metadataProfiles.error,
|
||||
(state) => state.settings.importLists.error,
|
||||
(state) => state.settings.indexerFlags.error,
|
||||
(state) => state.system.status.error,
|
||||
(state) => state.app.translations.error,
|
||||
(
|
||||
artistsError,
|
||||
customFiltersError,
|
||||
tagsError,
|
||||
uiSettingsError,
|
||||
|
@ -96,10 +112,12 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
metadataProfilesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
) => {
|
||||
const hasError = !!(
|
||||
artistsError ||
|
||||
customFiltersError ||
|
||||
tagsError ||
|
||||
uiSettingsError ||
|
||||
|
@ -107,6 +125,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError ||
|
||||
metadataProfilesError ||
|
||||
importListsError ||
|
||||
indexerFlagsError ||
|
||||
systemStatusError ||
|
||||
translationsError
|
||||
);
|
||||
|
@ -120,6 +139,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
metadataProfilesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
};
|
||||
|
@ -177,6 +197,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatchFetchImportLists() {
|
||||
dispatch(fetchImportLists());
|
||||
},
|
||||
dispatchFetchIndexerFlags() {
|
||||
dispatch(fetchIndexerFlags());
|
||||
},
|
||||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
|
@ -217,6 +240,7 @@ class PageConnector extends Component {
|
|||
this.props.dispatchFetchQualityProfiles();
|
||||
this.props.dispatchFetchMetadataProfiles();
|
||||
this.props.dispatchFetchImportLists();
|
||||
this.props.dispatchFetchIndexerFlags();
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
this.props.dispatchFetchTranslations();
|
||||
|
@ -243,6 +267,7 @@ class PageConnector extends Component {
|
|||
dispatchFetchQualityProfiles,
|
||||
dispatchFetchMetadataProfiles,
|
||||
dispatchFetchImportLists,
|
||||
dispatchFetchIndexerFlags,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
dispatchFetchTranslations,
|
||||
|
@ -284,6 +309,7 @@ PageConnector.propTypes = {
|
|||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue