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