Compare commits

..

No commits in common. "develop" and "v2.1.6.3993" have entirely different histories.

792 changed files with 9281 additions and 23173 deletions

View file

@ -1,13 +0,0 @@
// This file is used to open the backend and frontend in the same workspace, which is necessary as
// the frontend has vscode settings that are distinct from the backend
{
"folders": [
{
"path": ".."
},
{
"path": "../frontend"
}
],
"settings": {}
}

View file

@ -1,19 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "Lidarr",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true,
"version": "20",
"nvmVersion": "latest"
}
},
"forwardPorts": [8686],
"customizations": {
"vscode": {
"extensions": ["esbenp.prettier-vscode"]
}
}
}

View file

@ -60,7 +60,6 @@ body:
- Master
- Develop
- Nightly
- Plugins (experimental)
- Other (This issue will be closed)
validations:
required: true

View file

@ -1,12 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

View file

@ -12,6 +12,6 @@ jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/label-actions@v4
- uses: dessant/label-actions@v3
with:
process-only: 'issues'

View file

@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
- uses: dessant/lock-threads@v4
with:
github-token: ${{ github.token }}
issue-inactive-days: '90'

36
.gitignore vendored
View file

@ -121,13 +121,11 @@ _artifacts
_rawPackage/
_dotTrace*
_tests/
_temp*
*.Result.xml
coverage*.xml
coverage*.json
setup/Output/
*.~is
.mono
# VS outout folders
bin
@ -140,6 +138,12 @@ project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
#VS outout folders
bin
obj
output/*
# macOS metadata files
._*
.DS_Store
@ -158,12 +162,34 @@ Thumbs.db
/tools/Addins/*
packages.config.md5sum
# Common IntelliJ Platform excludes
# User specific
**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/.idea.Radarr.Posix
**/.idea/.idea.Radarr.Windows
# Sensitive or high-churn files
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
# ignore node_modules symlink
node_modules
node_modules.nosync
# API doc generation
.config/
# Ignore Jetbrains IntelliJ Workspace Directories
.idea/

View file

@ -1,7 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"ms-dotnettools.csdevkit",
"ms-vscode-remote.remote-containers"
]
}

26
.vscode/launch.json vendored
View file

@ -1,26 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": "Run Lidarr",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build dotnet",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/_output/net6.0/Lidarr",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

44
.vscode/tasks.json vendored
View file

@ -1,44 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build dotnet",
"command": "dotnet",
"type": "process",
"args": [
"msbuild",
"-restore",
"${workspaceFolder}/src/Lidarr.sln",
"-p:GenerateFullPaths=true",
"-p:Configuration=Debug",
"-p:Platform=Posix",
"-consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/Lidarr.sln",
"-property:GenerateFullPaths=true",
"-consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Lidarr.sln"
],
"problemMatcher": "$msCompile"
}
]
}

View file

@ -1,7 +1,6 @@
# Lidarr
[![Build Status](https://dev.azure.com/Lidarr/Lidarr/_apis/build/status/lidarr.Lidarr?branchName=develop)](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
[![Translation status](https://translate.servarr.com/widget/servarr/lidarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/lidarr.svg)](https://wiki.servarr.com/lidarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/lidarr/lidarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/lidarr/backers/badge.svg)](#backers)
@ -9,9 +8,6 @@
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
> [!WARNING]
> NOTICE - The Lidarr Metadata Server is currently down impacting adding artists, etc. Please follow [GHI 5498](https://github.com/Lidarr/Lidarr/issues/5498) or see Discord for detaila.
## Major Features Include:
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.

View file

@ -9,18 +9,18 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '2.13.3'
majorVersion: '2.1.6'
minorVersion: $[counter('minorVersion', 1076)]
lidarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.427'
nodeVersion: '20.X'
dotnetVersion: '6.0.417'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-22.04'
macImage: 'macOS-13'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
trigger:
branches:
@ -166,10 +166,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseNode@1
- task: NodeTool@0
displayName: Set Node.js version
inputs:
version: $(nodeVersion)
versionSpec: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@ -1093,10 +1093,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseNode@1
- task: NodeTool@0
displayName: Set Node.js version
inputs:
version: $(nodeVersion)
versionSpec: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@ -1120,19 +1120,19 @@ stages:
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@3
- task: SonarCloudPrepare@1
env:
SONAR_SCANNER_OPTS: ''
inputs:
SonarCloud: 'SonarCloud'
organization: 'lidarr'
scannerMode: 'cli'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'lidarr_Lidarr.UI'
cliProjectName: 'LidarrUI'
cliProjectVersion: '$(lidarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@3
- task: SonarCloudAnalyze@1
- job: Api_Docs
displayName: API Docs
@ -1208,12 +1208,12 @@ stages:
submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@3
- task: SonarCloudPrepare@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
organization: 'lidarr'
scannerMode: 'dotnet'
scannerMode: 'MSBuild'
projectKey: 'lidarr_Lidarr'
projectName: 'Lidarr'
projectVersion: '$(lidarrVersion)'
@ -1226,16 +1226,21 @@ stages:
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@3
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5.3.11
- task: reportgenerator@4
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
publishCodeCoverageResults: true
- task: PublishCodeCoverageResults@1
displayName: Publish Coverage Report
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
reportDirectory: './CoverageResults/combined/'
- stage: Report_Out
dependsOn:

View file

@ -1,182 +0,0 @@
#!/bin/bash
### Description: Lidarr .NET Debian install
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Lidarr installs
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
### Boilerplate Warning
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
scriptversion="1.0.3"
scriptdate="2024-01-06"
set -euo pipefail
echo "Running Lidarr Install Script - Version [$scriptversion] as of [$scriptdate]"
# Am I root?, need root!
if [ "$EUID" -ne 0 ]; then
echo "Please run as root."
exit
fi
app="lidarr"
app_port="8686"
app_prereq="curl sqlite3 wget"
app_umask="0002"
branch="main"
# Constants
### Update these variables as required for your specific instance
installdir="/opt" # {Update me if needed} Install Location
bindir="${installdir}/${app^}" # Full Path to Install Location
datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use
app_bin=${app^} # Binary Name of the app
# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir.
if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then
echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory"
exit
fi
# Prompt User
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
app_uid=$(echo "$app_uid" | tr -d ' ')
app_uid=${app_uid:-$app}
# Prompt Group
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
app_guid=$(echo "$app_guid" | tr -d ' ')
app_guid=${app_guid:-media}
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
# Create User / Group as needed
if [ "$app_guid" != "$app_uid" ]; then
if ! getent group "$app_guid" >/dev/null; then
groupadd "$app_guid"
fi
fi
if ! getent passwd "$app_uid" >/dev/null; then
adduser --system --no-create-home --ingroup "$app_guid" "$app_uid"
echo "Created and added User [$app_uid] to Group [$app_guid]"
fi
if ! getent group "$app_guid" | grep -qw "$app_uid"; then
echo "User [$app_uid] did not exist in Group [$app_guid]"
usermod -a -G "$app_guid" "$app_uid"
echo "Added User [$app_uid] to Group [$app_guid]"
fi
# Stop the App if running
if service --status-all | grep -Fq "$app"; then
systemctl stop "$app"
systemctl disable "$app".service
echo "Stopped existing $app"
fi
# Create Appdata Directory
# AppData
mkdir -p "$datadir"
chown -R "$app_uid":"$app_guid" "$datadir"
chmod 775 "$datadir"
echo "Directories created"
# Download and install the App
# prerequisite packages
echo ""
echo "Installing pre-requisite Packages"
# shellcheck disable=SC2086
apt update && apt install -y $app_prereq
echo ""
ARCH=$(dpkg --print-architecture)
# get arch
dlbase="https://lidarr.servarr.com/v1/update/$branch/updatefile?os=linux&runtime=netcore"
case "$ARCH" in
"amd64") DLURL="${dlbase}&arch=x64" ;;
"armhf") DLURL="${dlbase}&arch=arm" ;;
"arm64") DLURL="${dlbase}&arch=arm64" ;;
*)
echo "Arch not supported"
exit 1
;;
esac
echo ""
echo "Removing previous tarballs"
# -f to Force so we fail if it doesn't exist
rm -f "${app^}".*.tar.gz
echo ""
echo "Downloading..."
wget --content-disposition "$DLURL"
tar -xvzf "${app^}".*.tar.gz
echo ""
echo "Installation files downloaded and extracted"
# remove existing installs
echo "Removing existing installation"
rm -rf "$bindir"
echo "Installing..."
mv "${app^}" $installdir
chown "$app_uid":"$app_guid" -R "$bindir"
chmod 775 "$bindir"
rm -rf "${app^}.*.tar.gz"
# Ensure we check for an update in case user installs older version or different branch
touch "$datadir"/update_required
chown "$app_uid":"$app_guid" "$datadir"/update_required
echo "App Installed"
# Configure Autostart
# Remove any previous app .service
echo "Removing old service file"
rm -rf /etc/systemd/system/"$app".service
# Create app .service with correct user startup
echo "Creating service file"
cat <<EOF | tee /etc/systemd/system/"$app".service >/dev/null
[Unit]
Description=${app^} Daemon
After=syslog.target network.target
[Service]
User=$app_uid
Group=$app_guid
UMask=$app_umask
Type=simple
ExecStart=$bindir/$app_bin -nobrowser -data=$datadir
TimeoutStopSec=20
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
# Start the App
echo "Service file created. Attempting to start the app"
systemctl -q daemon-reload
systemctl enable --now -q "$app"
# Finish Update/Installation
host=$(hostname -I)
ip_local=$(grep -oP '^\S*' <<<"$host")
echo ""
echo "Install complete"
sleep 10
STATUS="$(systemctl is-active "$app")"
if [ "${STATUS}" = "active" ]; then
echo "Browse to http://$ip_local:$app_port for the ${app^} GUI"
else
echo "${app^} failed to start"
fi
# Exit
exit 0

View file

@ -1,20 +0,0 @@
# This file is owned by the lidarr package, DO NOT MODIFY MANUALLY
# Instead use 'dpkg-reconfigure -plow lidarr' to modify User/Group/UMask/-data
# Or use systemd built-in override functionality using 'systemctl edit lidarr'
[Unit]
Description=Lidarr Daemon
After=network.target
[Service]
User=lidarr
Group=lidarr
UMask=002
Type=simple
ExecStart=/opt/Lidarr/Lidarr -nobrowser -data=/var/lib/lidarr
TimeoutStopSec=20
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target

23
docs.sh
View file

@ -1,18 +1,13 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1
ARCHITECTURE="${2:-x64}"
if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-$ARCHITECTURE"
RUNTIME="win-x64"
elif [ "$PLATFORM" = "Linux" ]; then
RUNTIME="linux-$ARCHITECTURE"
RUNTIME="linux-x64"
elif [ "$PLATFORM" = "Mac" ]; then
RUNTIME="osx-$ARCHITECTURE"
RUNTIME="osx-x64"
else
echo "Platform must be provided as first argument: Windows, Linux or Mac"
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
exit 1
fi
@ -26,21 +21,15 @@ slnFile=src/Lidarr.sln
platform=Posix
if [ "$PLATFORM" = "Windows" ]; then
application=Lidarr.Console.dll
else
application=Lidarr.dll
fi
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/lidarr.console.dll" v1 &
sleep 45

View file

@ -28,8 +28,7 @@ module.exports = {
globals: {
expect: false,
chai: false,
sinon: false,
JSX: true
sinon: false
},
parserOptions: {

View file

@ -9,7 +9,7 @@
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
"source.fixAll": true
},
"typescript.preferences.quoteStyle": "single",

View file

@ -26,7 +26,6 @@ module.exports = (env) => {
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: {
children: false
@ -68,7 +67,7 @@ module.exports = (env) => {
output: {
path: distFolder,
publicPath: '/',
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
filename: '[name]-[contenthash].js',
sourceMapFilename: '[file].map'
},
@ -93,7 +92,7 @@ module.exports = (env) => {
new MiniCssExtractPlugin({
filename: 'Content/styles.css',
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
chunkFilename: 'Content/[id]-[chunkhash].css'
}),
new HtmlWebpackPlugin({
@ -135,12 +134,6 @@ module.exports = (env) => {
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
},
// manifest.json and browserconfig.xml
{
source: 'frontend/src/Content/*.(json|xml)',
destination: path.join(distFolder, 'Content')
}
]
}
@ -188,7 +181,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: '3.41'
corejs: 3
}
]
]
@ -209,7 +202,7 @@ module.exports = (env) => {
options: {
importLoaders: 1,
modules: {
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},

View file

@ -16,7 +16,6 @@ const mixinsFiles = [
module.exports = {
plugins: [
'autoprefixer',
['postcss-mixins', {
mixinsFiles
}],

View file

@ -172,8 +172,7 @@ function HistoryDetails(props) {
if (eventType === 'downloadFailed') {
const {
message,
indexer
message
} = data;
return (
@ -193,14 +192,6 @@ function HistoryDetails(props) {
null
}
{
indexer ? (
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
) : null}
{
message ?
<DescriptionListItem

View file

@ -26,5 +26,4 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 90px;
text-align: right;
}

View file

@ -57,40 +57,30 @@ function QueueStatusCell(props) {
if (status === 'paused') {
iconName = icons.PAUSED;
title = translate('Paused');
title = 'Paused';
}
if (status === 'queued') {
iconName = icons.QUEUED;
title = translate('Queued');
title = 'Queued';
}
if (status === 'completed') {
iconName = icons.DOWNLOADED;
title = translate('Downloaded');
if (trackedDownloadState === 'importBlocked') {
title += ` - ${translate('UnableToImportAutomatically')}`;
iconKind = kinds.WARNING;
}
if (trackedDownloadState === 'importFailed') {
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
iconKind = kinds.WARNING;
}
title = 'Downloaded';
if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`;
title += ' - Waiting to Import';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ` - ${translate('Importing')}`;
title += ' - Importing';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ` - ${translate('WaitingToProcess')}`;
title += ' - Waiting to Process';
iconKind = kinds.DANGER;
}
}
@ -101,38 +91,36 @@ function QueueStatusCell(props) {
if (status === 'delay') {
iconName = icons.PENDING;
title = translate('Pending');
title = 'Pending';
}
if (status === 'downloadClientUnavailable') {
iconName = icons.PENDING;
iconKind = kinds.WARNING;
title = translate('PendingDownloadClientUnavailable');
title = 'Pending - Download client is unavailable';
}
if (status === 'failed') {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
title = 'Download failed';
}
if (status === 'warning') {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
const warningMessage =
errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', { warningMessage });
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
}
if (hasError) {
if (status === 'completed') {
iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER;
title = translate('ImportFailed', { sourceTitle });
title = `Import failed: ${sourceTitle}`;
} else {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
title = 'Download failed';
}
}

View file

@ -118,7 +118,6 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
{
key: 'blocklistAndSearch',
value: translate('BlocklistAndSearch'),
isDisabled: isPending,
hint: multipleSelected
? translate('BlocklistAndSearchMultipleHint')
: translate('BlocklistAndSearchHint'),
@ -131,7 +130,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
: translate('BlocklistOnlyHint'),
},
];
}, [isPending, multipleSelected]);
}, [multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {

View file

@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
<DescriptionList>
<DescriptionListItem
title={translate('AllAlbums')}
data={translate('MonitorAllAlbums')}
data="Monitor all new albums"
/>
<DescriptionListItem
title={translate('NewAlbums')}
data={translate('MonitorNewAlbumsData')}
data="Monitor new albums released after the newest existing album"
/>
<DescriptionListItem
title={translate('None')}
data={translate('MonitorNoAlbumsData')}
data="Don't monitor any new albums"
/>
</DescriptionList>
);

View file

@ -10,7 +10,6 @@ export interface Statistics {
}
interface Album extends ModelBase {
artistId: number;
artist: Artist;
foreignAlbumId: string;
title: string;
@ -20,7 +19,6 @@ interface Album extends ModelBase {
monitored: boolean;
releaseDate: string;
statistics: Statistics;
lastSearchTime?: string;
isSaving?: boolean;
}

View file

@ -4,11 +4,10 @@ import Link from 'Components/Link/Link';
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
const link = `/album/${foreignAlbumId}`;
const albumTitle = `${title}${disambiguation ? ` (${disambiguation})` : ''}`;
return (
<Link to={link} title={albumTitle}>
{albumTitle}
<Link to={link}>
{title}{disambiguation ? ` (${disambiguation})` : ''}
</Link>
);
}

View file

@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
render() {
const {
title,
statistics = {},
statistics,
onModalClose
} = this.props;

View file

@ -121,8 +121,6 @@
.releaseDate,
.sizeOnDisk,
.albumType,
.secondaryTypes,
.qualityProfileName,
.links,
.tags {

View file

@ -3,7 +3,6 @@
interface CssExports {
'albumNavigationButton': string;
'albumNavigationButtons': string;
'albumType': string;
'alternateTitlesIconContainer': string;
'backdrop': string;
'backdropOverlay': string;
@ -21,7 +20,6 @@ interface CssExports {
'overview': string;
'qualityProfileName': string;
'releaseDate': string;
'secondaryTypes': string;
'sizeOnDisk': string;
'tags': string;
'title': string;

View file

@ -192,7 +192,6 @@ class AlbumDetails extends Component {
duration,
overview,
albumType,
secondaryTypes,
statistics = {},
monitored,
releaseDate,
@ -205,7 +204,6 @@ class AlbumDetails extends Component {
isFetching,
isPopulated,
albumsError,
tracksError,
trackFilesError,
hasTrackFiles,
shortDateFormat,
@ -398,11 +396,10 @@ class AlbumDetails extends Component {
<div className={styles.details}>
<div>
{
duration ?
!!duration &&
<span className={styles.duration}>
{formatDuration(duration)}
</span> :
null
</span>
}
<HeartRating
@ -421,15 +418,14 @@ class AlbumDetails extends Component {
title={translate('ReleaseDate')}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.CALENDAR}
size={17}
/>
<span className={styles.releaseDate}>
{moment(releaseDate).format(shortDateFormat)}
</span>
</div>
</Label>
<Tooltip
@ -438,15 +434,16 @@ class AlbumDetails extends Component {
className={styles.detailsLabel}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.DRIVE}
size={17}
/>
<span className={styles.sizeOnDisk}>
{formatBytes(sizeOnDisk)}
{
formatBytes(sizeOnDisk || 0)
}
</span>
</div>
</Label>
}
tooltip={
@ -462,55 +459,32 @@ class AlbumDetails extends Component {
className={styles.detailsLabel}
size={sizes.LARGE}
>
<div>
<Icon
name={monitored ? icons.MONITORED : icons.UNMONITORED}
size={17}
/>
<span className={styles.qualityProfileName}>
{monitored ? translate('Monitored') : translate('Unmonitored')}
</span>
</div>
</Label>
{
albumType ?
!!albumType &&
<Label
className={styles.detailsLabel}
title={translate('Type')}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.INFO}
size={17}
/>
<span className={styles.albumType}>
<span className={styles.qualityProfileName}>
{albumType}
</span>
</div>
</Label> :
null
}
{
secondaryTypes.length ?
<Label
className={styles.detailsLabel}
title={translate('SecondaryTypes')}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.INFO}
size={17}
/>
<span className={styles.secondaryTypes}>
{secondaryTypes.join(', ')}
</span>
</div>
</Label> :
null
</Label>
}
<Tooltip
@ -519,15 +493,14 @@ class AlbumDetails extends Component {
className={styles.detailsLabel}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.EXTERNAL_LINK}
size={17}
/>
<span className={styles.links}>
{translate('Links')}
</span>
</div>
</Label>
}
tooltip={
@ -553,9 +526,8 @@ class AlbumDetails extends Component {
<div className={styles.contentContainer}>
{
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
<LoadingIndicator /> :
null
!isPopulated && !albumsError && !trackFilesError &&
<LoadingIndicator />
}
{
@ -566,14 +538,6 @@ class AlbumDetails extends Component {
null
}
{
!isFetching && tracksError ?
<Alert kind={kinds.DANGER}>
{translate('TracksLoadError')}
</Alert> :
null
}
{
!isFetching && trackFilesError ?
<Alert kind={kinds.DANGER}>
@ -602,14 +566,6 @@ class AlbumDetails extends Component {
</div>
}
{
isPopulated && !media.length ?
<Alert kind={kinds.WARNING}>
{translate('NoMediumInformation')}
</Alert> :
null
}
</div>
<OrganizePreviewModalConnector
@ -676,7 +632,6 @@ AlbumDetails.propTypes = {
duration: PropTypes.number,
overview: PropTypes.string,
albumType: PropTypes.string.isRequired,
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
statistics: PropTypes.object.isRequired,
releaseDate: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired,
@ -703,8 +658,6 @@ AlbumDetails.propTypes = {
};
AlbumDetails.defaultProps = {
secondaryTypes: [],
statistics: {},
isSaving: false
};

View file

@ -12,13 +12,16 @@ import TrackRowConnector from './TrackRowConnector';
import styles from './AlbumDetailsMedium.css';
function getMediumStatistics(tracks) {
const trackCount = tracks.length;
let trackCount = 0;
let trackFileCount = 0;
let totalTrackCount = 0;
tracks.forEach((track) => {
if (track.trackFileId) {
trackCount++;
trackFileCount++;
} else {
trackCount++;
}
totalTrackCount++;
@ -172,7 +175,7 @@ class AlbumDetailsMedium extends Component {
</Table> :
<div className={styles.noTracks}>
{translate('NoTracksInThisMedium')}
No tracks in this medium
</div>
}
<div className={styles.collapseButtonContainer}>

View file

@ -35,9 +35,3 @@
width: 55px;
}
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}

View file

@ -4,7 +4,6 @@ interface CssExports {
'audio': string;
'customFormatScore': string;
'duration': string;
'indexerFlags': string;
'monitored': string;
'size': string;
'status': string;

View file

@ -2,19 +2,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AlbumFormats from 'Album/AlbumFormats';
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
import IndexerFlags from 'Album/IndexerFlags';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { tooltipPositions } from 'Helpers/Props';
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import TrackActionsCell from './TrackActionsCell';
import styles from './TrackRow.css';
@ -36,7 +32,6 @@ class TrackRow extends Component {
trackFileSize,
customFormats,
customFormatScore,
indexerFlags,
columns,
deleteTrackFile
} = this.props;
@ -146,30 +141,12 @@ class TrackRow extends Component {
customFormats.length
)}
tooltip={<AlbumFormats formats={customFormats} />}
position={tooltipPositions.LEFT}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'size') {
return (
<TableRowCell
@ -231,14 +208,12 @@ TrackRow.propTypes = {
trackFileSize: PropTypes.number,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired
};
TrackRow.defaultProps = {
customFormats: [],
indexerFlags: 0
customFormats: []
};
export default TrackRow;

View file

@ -13,8 +13,7 @@ function createMapStateToProps() {
trackFilePath: trackFile ? trackFile.path : null,
trackFileSize: trackFile ? trackFile.size : null,
customFormats: trackFile ? trackFile.customFormats : [],
customFormatScore: trackFile ? trackFile.customFormatScore : 0,
indexerFlags: trackFile ? trackFile.indexerFlags : 0
customFormatScore: trackFile ? trackFile.customFormatScore : 0
};
}
);

View file

@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
title,
artistName,
albumType,
statistics = {},
statistics,
item,
isSaving,
onInputChange,

View file

@ -1,26 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const flags = allIndexerFlags.items.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
})}
</ul>
) : null;
}
export default IndexerFlags;

View file

@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_EXTRA_LARGE}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>

View file

@ -7,7 +7,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { scrollDirections } from 'Helpers/Props';
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
import translate from 'Utilities/String/translate';
function AlbumInteractiveSearchModalContent(props) {
const {
@ -19,10 +18,7 @@ function AlbumInteractiveSearchModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{albumTitle === undefined ?
translate('InteractiveSearchModalHeader') :
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
}
Interactive Search {albumId != null && `- ${albumTitle}`}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
@ -36,7 +32,7 @@ function AlbumInteractiveSearchModalContent(props) {
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
Close
</Button>
</ModalFooter>
</ModalContent>

View file

@ -12,10 +12,11 @@ function App({ store, history }) {
<DocumentTitle title={window.Lidarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme />
<ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
</ConnectedRouter>
</Provider>
</DocumentTitle>

View file

@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import AddNewItemConnector from 'Search/AddNewItemConnector';
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import Updates from 'System/Updates/Updates';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
@ -184,7 +184,7 @@ function AppRoutes(props) {
<Route
path="/settings/customformats"
component={CustomFormatSettingsPage}
component={CustomFormatSettingsConnector}
/>
<Route
@ -248,7 +248,7 @@ function AppRoutes(props) {
<Route
path="/system/updates"
component={Updates}
component={UpdatesConnector}
/>
<Route

View file

@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Lidarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);

View file

@ -1,37 +0,0 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme,
(theme) => {
return theme;
}
);
}
function ApplyTheme({ children }: ApplyThemeProps) {
const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => {
Object.entries(themes[theme]).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables();
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
export default ApplyTheme;

View file

@ -1,12 +1,8 @@
import ParseAppState from 'App/State/ParseAppState';
import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import QueueAppState from './QueueAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
import TrackFilesAppState from './TrackFilesAppState';
import TracksAppState from './TracksAppState';
@ -44,7 +40,6 @@ export interface CustomFilter {
}
export interface AppSectionState {
version: string;
dimensions: {
isSmallScreen: boolean;
width: number;
@ -57,16 +52,12 @@ interface AppState {
app: AppSectionState;
artist: ArtistAppState;
artistIndex: ArtistIndexAppState;
calendar: CalendarAppState;
commands: CommandAppState;
history: HistoryAppState;
parse: ParseAppState;
queue: QueueAppState;
settings: SettingsAppState;
tags: TagsAppState;
trackFiles: TrackFilesAppState;
tracksSelection: TracksAppState;
system: SystemAppState;
}
export default AppState;

View file

@ -1,10 +0,0 @@
import Album from 'Album/Album';
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
interface CalendarAppState
extends AppSectionState<Album>,
AppSectionFilterState<Album> {}
export default CalendarAppState;

View file

@ -1,6 +0,0 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View file

@ -1,35 +0,0 @@
import Album from 'Album/Album';
import ModelBase from 'App/ModelBase';
import { AppSectionItemState } from 'App/State/AppSectionState';
import Artist from 'Artist/Artist';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
export interface ArtistTitleInfo {
title: string;
}
export interface ParsedAlbumInfo {
albumTitle: string;
artistName: string;
artistTitleInfo: ArtistTitleInfo;
discography: boolean;
quality: QualityModel;
releaseGroup?: string;
releaseHash: string;
releaseTitle: string;
releaseTokens: string;
}
export interface ParseModel extends ModelBase {
title: string;
parsedAlbumInfo: ParsedAlbumInfo;
artist?: Artist;
albums: Album[];
customFormats?: CustomFormat[];
customFormatScore?: number;
}
type ParseAppState = AppSectionItemState<ParseModel>;
export default ParseAppState;

View file

@ -1,28 +1,22 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
} from 'App/State/AppSectionState';
import CustomFormat from 'typings/CustomFormat';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import MetadataProfile from 'typings/MetadataProfile';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import RootFolder from 'typings/RootFolder';
import General from 'typings/Settings/General';
import UiSettings from 'typings/Settings/UiSettings';
import { UiSettings } from 'typings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
export type GeneralAppState = AppSectionItemState<General>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
@ -45,32 +39,22 @@ export interface MetadataProfilesAppState
extends AppSectionState<MetadataProfile>,
AppSectionSchemaState<MetadataProfile> {}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface RootFolderAppState
extends AppSectionState<RootFolder>,
AppSectionDeleteState,
AppSectionSaveState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
customFormats: CustomFormatAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
metadataProfiles: MetadataProfilesAppState;
notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState;
rootFolders: RootFolderAppState;
ui: UiSettingsAppState;
uiSettings: UiSettingsAppState;
}
export default SettingsAppState;

View file

@ -1,13 +0,0 @@
import SystemStatus from 'typings/SystemStatus';
import Update from 'typings/Update';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
updates: UpdateAppState;
status: SystemStatusAppState;
}
export default SystemAppState;

View file

@ -1,32 +1,12 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
export interface TagDetail extends ModelBase {
label: string;
autoTagIds: number[];
delayProfileIds: number[];
downloadClientIds: [];
importListIds: number[];
indexerIds: number[];
notificationIds: number[];
restrictionIds: number[];
artistIds: number[];
}
export interface TagDetailAppState
extends AppSectionState<TagDetail>,
AppSectionDeleteState,
AppSectionSaveState {}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
details: TagDetailAppState;
}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
export default TagsAppState;

View file

@ -23,6 +23,7 @@ export interface Ratings {
interface Artist extends ModelBase {
added: string;
artistMetadataId: string;
foreignArtistId: string;
cleanName: string;
ended: boolean;
@ -35,7 +36,6 @@ interface Artist extends ModelBase {
nextAlbum?: Album;
qualityProfileId: number;
metadataProfileId: number;
monitorNewItems: string;
ratings: Ratings;
rootFolderPath: string;
sortName: string;

View file

@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
Close
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteArtistConfirmed}
>
{translate('Delete')}
Delete
</Button>
</ModalFooter>
</ModalContent>
@ -161,7 +161,9 @@ DeleteArtistModalContent.propTypes = {
};
DeleteArtistModalContent.defaultProps = {
statistics: {}
statistics: {
trackFileCount: 0
}
};
export default DeleteArtistModalContent;

View file

@ -10,7 +10,6 @@ function AlbumGroupInfo(props) {
const {
totalAlbumCount,
monitoredAlbumCount,
albumFileCount,
trackFileCount,
sizeOnDisk
} = props;
@ -31,13 +30,6 @@ function AlbumGroupInfo(props) {
data={monitoredAlbumCount}
/>
<DescriptionListItem
titleClassName={styles.title}
descriptionClassName={styles.description}
title={translate('WithFiles')}
data={albumFileCount}
/>
<DescriptionListItem
titleClassName={styles.title}
descriptionClassName={styles.description}
@ -58,7 +50,6 @@ function AlbumGroupInfo(props) {
AlbumGroupInfo.propTypes = {
totalAlbumCount: PropTypes.number.isRequired,
monitoredAlbumCount: PropTypes.number.isRequired,
albumFileCount: PropTypes.number.isRequired,
trackFileCount: PropTypes.number.isRequired,
sizeOnDisk: PropTypes.number.isRequired
};

View file

@ -10,7 +10,6 @@
width: 42px;
}
.size,
.status {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View file

@ -2,7 +2,6 @@
// Please do not change this file!
interface CssExports {
'monitored': string;
'size': string;
'status': string;
'title': string;
}

View file

@ -10,7 +10,6 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { kinds, sizes } from 'Helpers/Props';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './AlbumRow.css';
@ -88,8 +87,7 @@ class AlbumRow extends Component {
const {
trackCount = 0,
trackFileCount = 0,
totalTrackCount = 0,
sizeOnDisk = 0
totalTrackCount = 0
} = statistics;
return (
@ -149,7 +147,9 @@ class AlbumRow extends Component {
if (name === 'secondaryTypes') {
return (
<TableRowCell key={name}>
{secondaryTypes.join(', ')}
{
secondaryTypes
}
</TableRowCell>
);
}
@ -158,7 +158,7 @@ class AlbumRow extends Component {
return (
<TableRowCell key={name}>
{
totalTrackCount
statistics.totalTrackCount
}
</TableRowCell>
);
@ -196,17 +196,6 @@ class AlbumRow extends Component {
);
}
if (name === 'size') {
return (
<TableRowCell
key={name}
className={styles.size}
>
{!!sizeOnDisk && formatBytes(sizeOnDisk)}
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell

View file

@ -192,7 +192,7 @@ class ArtistDetails extends Component {
artistName,
ratings,
path,
statistics = {},
statistics,
qualityProfileId,
monitored,
genres,

View file

@ -22,43 +22,32 @@ import styles from './ArtistDetailsSeason.css';
function getAlbumStatistics(albums) {
let albumCount = 0;
let albumFileCount = 0;
let trackFileCount = 0;
let totalAlbumCount = 0;
let monitoredAlbumCount = 0;
let hasMonitoredAlbums = false;
let sizeOnDisk = 0;
albums.forEach(({ monitored, releaseDate, statistics = {} }) => {
const {
trackFileCount: albumTrackFileCount = 0,
totalTrackCount: albumTotalTrackCount = 0,
sizeOnDisk: albumSizeOnDisk = 0
} = statistics;
albums.forEach((album) => {
if (album.statistics) {
sizeOnDisk = sizeOnDisk + album.statistics.sizeOnDisk;
trackFileCount = trackFileCount + album.statistics.trackFileCount;
const hasFiles = albumTrackFileCount > 0 && albumTrackFileCount === albumTotalTrackCount;
if (hasFiles || (monitored && isBefore(releaseDate))) {
if (album.statistics.trackFileCount === album.statistics.totalTrackCount || (album.monitored && isBefore(album.airDateUtc))) {
albumCount++;
}
if (hasFiles) {
albumFileCount++;
}
if (monitored) {
if (album.monitored) {
monitoredAlbumCount++;
hasMonitoredAlbums = true;
}
totalAlbumCount++;
trackFileCount = trackFileCount + albumTrackFileCount;
sizeOnDisk = sizeOnDisk + albumSizeOnDisk;
});
return {
albumCount,
albumFileCount,
totalAlbumCount,
trackFileCount,
monitoredAlbumCount,
@ -67,8 +56,8 @@ function getAlbumStatistics(albums) {
};
}
function getAlbumCountKind(monitored, albumCount, albumFileCount) {
if (albumCount === albumFileCount && albumFileCount > 0) {
function getAlbumCountKind(monitored, albumCount, monitoredAlbumCount) {
if (albumCount === monitoredAlbumCount && monitoredAlbumCount > 0) {
return kinds.SUCCESS;
}
@ -203,12 +192,11 @@ class ArtistDetailsSeason extends Component {
const {
albumCount,
albumFileCount,
totalAlbumCount,
trackFileCount,
monitoredAlbumCount,
hasMonitoredAlbums,
sizeOnDisk = 0
sizeOnDisk
} = getAlbumStatistics(items);
const {
@ -238,9 +226,9 @@ class ArtistDetailsSeason extends Component {
anchor={
<Label
size={sizes.LARGE}
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, albumFileCount)}
kind={getAlbumCountKind(hasMonitoredAlbums, albumCount, monitoredAlbumCount)}
>
<span>{albumFileCount} / {albumCount}</span>
<span>{albumCount} / {monitoredAlbumCount}</span>
</Label>
}
title={translate('GroupInformation')}
@ -249,7 +237,6 @@ class ArtistDetailsSeason extends Component {
<AlbumGroupInfo
totalAlbumCount={totalAlbumCount}
monitoredAlbumCount={monitoredAlbumCount}
albumFileCount={albumFileCount}
trackFileCount={trackFileCount}
sizeOnDisk={sizeOnDisk}
/>

View file

@ -2,7 +2,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import ArtistTags from './ArtistTags';
function createMapStateToProps() {
@ -13,8 +12,8 @@ function createMapStateToProps() {
const tags = artist.tags
.map((tagId) => tagList.find((tag) => tag.id === tagId))
.filter((tag) => !!tag)
.sort(sortByProp('label'))
.map((tag) => tag.label);
.map((tag) => tag.label)
.sort((a, b) => a.localeCompare(b));
return {
tags

View file

@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './EditArtistModalContent.css';
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
<ModalBody>
<Form {...otherProps}>
<FormGroup size={sizes.MEDIUM}>
<FormGroup>
<FormLabel>
{translate('Monitored')}
</FormLabel>
@ -107,10 +107,9 @@ class EditArtistModalContent extends Component {
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormGroup>
<FormLabel>
{translate('MonitorNewItems')}
<Popover
anchor={
<Icon
@ -133,7 +132,7 @@ class EditArtistModalContent extends Component {
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormGroup>
<FormLabel>
{translate('QualityProfile')}
</FormLabel>
@ -147,10 +146,10 @@ class EditArtistModalContent extends Component {
</FormGroup>
{
showMetadataProfile ?
<FormGroup size={sizes.MEDIUM}>
showMetadataProfile &&
<FormGroup>
<FormLabel>
{translate('MetadataProfile')}
Metadata Profile
<Popover
anchor={
@ -174,11 +173,10 @@ class EditArtistModalContent extends Component {
{...metadataProfileId}
onChange={onInputChange}
/>
</FormGroup> :
null
</FormGroup>
}
<FormGroup size={sizes.MEDIUM}>
<FormGroup>
<FormLabel>
{translate('Path')}
</FormLabel>
@ -191,7 +189,7 @@ class EditArtistModalContent extends Component {
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormGroup>
<FormLabel>
{translate('Tags')}
</FormLabel>
@ -211,7 +209,7 @@ class EditArtistModalContent extends Component {
kind={kinds.DANGER}
onPress={onDeleteArtistPress}
>
{translate('Delete')}
Delete
</Button>
<Button

View file

@ -7,8 +7,6 @@ import React, {
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import NoArtist from 'Artist/NoArtist';
import { RSS_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -91,19 +89,16 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
sortKey,
sortDirection,
view,
}: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState =
useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
} = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
const isRssSyncExecuting = useSelector(
createCommandExecutingSelector(RSS_SYNC)
);
const { isSmallScreen } = useSelector(createDimensionsSelector());
const dispatch = useDispatch();
const scrollerRef = useRef<HTMLDivElement>(null);
const scrollerRef = useRef<HTMLDivElement>();
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
undefined
);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [isSelectMode, setIsSelectMode] = useState(false);
useEffect(() => {
@ -123,14 +118,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback(
(payload: unknown) => {
(payload) => {
dispatch(setArtistTableOption(payload));
},
[dispatch]
);
const onViewSelect = useCallback(
(value: string) => {
(value) => {
dispatch(setArtistView({ view: value }));
if (scrollerRef.current) {
@ -141,14 +136,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
);
const onSortSelect = useCallback(
(value: string) => {
(value) => {
dispatch(setArtistSort({ sortKey: value }));
},
[dispatch]
);
const onFilterSelect = useCallback(
(value: string) => {
(value) => {
dispatch(setArtistFilter({ selectedFilterKey: value }));
},
[dispatch]
@ -163,15 +158,15 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
}, [setIsOptionsModalOpen]);
const onJumpBarItemPress = useCallback(
(character: string) => {
(character) => {
setJumpToCharacter(character);
},
[setJumpToCharacter]
);
const onScroll = useCallback(
({ scrollTop }: { scrollTop: number }) => {
setJumpToCharacter(undefined);
({ scrollTop }) => {
setJumpToCharacter(null);
scrollPositions.artistIndex = scrollTop;
},
[setJumpToCharacter]
@ -185,10 +180,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
};
}
const characters = items.reduce((acc: Record<string, number>, item) => {
const characters = items.reduce((acc, item) => {
let char = item.sortName.charAt(0);
if (!isNaN(Number(char))) {
if (!isNaN(char)) {
char = '#';
}
@ -305,8 +300,6 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}

View file

@ -23,13 +23,7 @@ function createFilterBuilderPropsSelector() {
);
}
interface ArtistIndexFilterModalProps {
isOpen: boolean;
}
export default function ArtistIndexFilterModal(
props: ArtistIndexFilterModalProps
) {
export default function ArtistIndexFilterModal(props) {
const sectionItems = useSelector(createArtistSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'artist';
@ -37,7 +31,7 @@ export default function ArtistIndexFilterModal(
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
(payload) => {
dispatch(setArtistFilter(payload));
},
[dispatch]
@ -45,7 +39,6 @@ export default function ArtistIndexFilterModal(
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}

View file

@ -206,7 +206,7 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
</div>
) : null}
{showQualityProfile && !!qualityProfile?.name ? (
{showQualityProfile ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>

View file

@ -1,9 +1,8 @@
import { throttle } from 'lodash';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner';
import useMeasure from 'Helpers/Hooks/useMeasure';
@ -22,7 +21,7 @@ const columnPaddingSmallScreen = parseInt(
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
const ADDITIONAL_COLUMN_COUNT = {
small: 3,
medium: 2,
large: 1,
@ -42,17 +41,17 @@ interface CellItemData {
interface ArtistIndexBannersProps {
items: Artist[];
sortKey: string;
sortKey?: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: RefObject<HTMLElement>;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const artistIndexSelector = createSelector(
(state: AppState) => state.artistIndex.bannerOptions,
(state) => state.artistIndex.bannerOptions,
(bannerOptions) => {
return {
bannerOptions,
@ -109,7 +108,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
} = props;
const { bannerOptions } = useSelector(artistIndexSelector);
const ref = useRef<Grid>(null);
const ref: React.MutableRefObject<Grid> = useRef();
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -223,8 +222,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -233,7 +232,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -256,8 +255,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
const scrollTop = rowIndex * rowHeight + padding;
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current?.scrollTo(0, scrollTop);
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current.scrollTo(0, scrollTop);
}
}
}, [

View file

@ -59,7 +59,7 @@ function ArtistIndexBannerOptionsModalContent(
const dispatch = useDispatch();
const onBannerOptionChange = useCallback(
({ name, value }: { name: string; value: unknown }) => {
({ name, value }) => {
dispatch(setArtistBannerOption({ [name]: value }));
},
[dispatch]

View file

@ -1,18 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import { CustomFilter } from 'App/State/AppState';
import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
interface ArtistIndexFilterMenuProps {
selectedFilterKey: string | number;
filters: object[];
customFilters: CustomFilter[];
isDisabled: boolean;
onFilterSelect(filterName: string): unknown;
}
function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) {
function ArtistIndexFilterMenu(props) {
const {
selectedFilterKey,
filters,
@ -34,6 +26,15 @@ function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) {
);
}
ArtistIndexFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
};
ArtistIndexFilterMenu.defaultProps = {
showCustomFilters: false,
};

View file

@ -1,19 +1,11 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem';
import { align } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import translate from 'Utilities/String/translate';
import { align, sortDirections } from 'Helpers/Props';
interface SeriesIndexSortMenuProps {
sortKey?: string;
sortDirection?: SortDirection;
isDisabled: boolean;
onSortSelect(sortKey: string): unknown;
}
function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
function ArtistIndexSortMenu(props) {
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
return (
@ -25,7 +17,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('MonitoredStatus')}
Monitored/Status
</SortMenuItem>
<SortMenuItem
@ -34,7 +26,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Name')}
Name
</SortMenuItem>
<SortMenuItem
@ -43,7 +35,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Type')}
Type
</SortMenuItem>
<SortMenuItem
@ -52,7 +44,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('QualityProfile')}
Quality Profile
</SortMenuItem>
<SortMenuItem
@ -61,7 +53,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('MetadataProfile')}
Metadata Profile
</SortMenuItem>
<SortMenuItem
@ -70,7 +62,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('NextAlbum')}
Next Album
</SortMenuItem>
<SortMenuItem
@ -79,7 +71,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('LastAlbum')}
Last Album
</SortMenuItem>
<SortMenuItem
@ -88,7 +80,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Added')}
Added
</SortMenuItem>
<SortMenuItem
@ -97,7 +89,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Albums')}
Albums
</SortMenuItem>
<SortMenuItem
@ -106,7 +98,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Tracks')}
Tracks
</SortMenuItem>
<SortMenuItem
@ -115,7 +107,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('TrackCount')}
Track Count
</SortMenuItem>
<SortMenuItem
@ -124,7 +116,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Path')}
Path
</SortMenuItem>
<SortMenuItem
@ -133,7 +125,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('SizeOnDisk')}
Size on Disk
</SortMenuItem>
<SortMenuItem
@ -142,11 +134,18 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Tags')}
Tags
</SortMenuItem>
</MenuContent>
</SortMenu>
);
}
ArtistIndexSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
};
export default ArtistIndexSortMenu;

View file

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenu from 'Components/Menu/ViewMenu';
@ -5,13 +6,7 @@ import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
interface ArtistIndexViewMenuProps {
view: string;
isDisabled: boolean;
onViewSelect(value: string): unknown;
}
function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) {
function ArtistIndexViewMenu(props) {
const { view, isDisabled, onViewSelect } = props;
return (
@ -41,4 +36,10 @@ function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) {
);
}
ArtistIndexViewMenu.propTypes = {
view: PropTypes.string.isRequired,
isDisabled: PropTypes.bool.isRequired,
onViewSelect: PropTypes.func.isRequired,
};
export default ArtistIndexViewMenu;

View file

@ -1,51 +1,15 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import Album from 'Album/Album';
import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import dimensions from 'Styles/Variables/dimensions';
import QualityProfile from 'typings/QualityProfile';
import UiSettings from 'typings/Settings/UiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
import styles from './ArtistIndexOverviewInfo.css';
interface RowProps {
name: string;
showProp: string;
valueProp: string;
}
interface RowInfoProps {
title: string;
iconName: IconDefinition;
label: string;
}
interface ArtistIndexOverviewInfoProps {
height: number;
showMonitored: boolean;
showQualityProfile: boolean;
showLastAlbum: boolean;
showAdded: boolean;
showAlbumCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
monitored: boolean;
nextAlbum?: Album;
qualityProfile?: QualityProfile;
lastAlbum?: Album;
added?: string;
albumCount: number;
path: string;
sizeOnDisk?: number;
sortKey: string;
}
const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
const rows = [
@ -86,17 +50,11 @@ const rows = [
},
];
function getInfoRowProps(
row: RowProps,
props: ArtistIndexOverviewInfoProps,
uiSettings: UiSettings
): RowInfoProps | null {
function getInfoRowProps(row, props, uiSettings) {
const { name } = row;
if (name === 'monitored') {
const monitoredText = props.monitored
? translate('Monitored')
: translate('Unmonitored');
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
return {
title: monitoredText,
@ -105,9 +63,9 @@ function getInfoRowProps(
};
}
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
if (name === 'qualityProfileId') {
return {
title: translate('QualityProfile'),
title: 'Quality Profile',
iconName: icons.PROFILE,
label: props.qualityProfile.name,
};
@ -120,8 +78,7 @@ function getInfoRowProps(
return {
title: `Last Album: ${lastAlbum.title}`,
iconName: icons.CALENDAR,
label:
getRelativeDate(
label: getRelativeDate(
lastAlbum.releaseDate,
shortDateFormat,
showRelativeDates,
@ -129,7 +86,7 @@ function getInfoRowProps(
timeFormat,
timeForToday: true,
}
) ?? '',
),
};
}
@ -141,11 +98,10 @@ function getInfoRowProps(
return {
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
iconName: icons.ADD,
label:
getRelativeDate(added, shortDateFormat, showRelativeDates, {
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: true,
}) ?? '',
}),
};
}
@ -160,7 +116,7 @@ function getInfoRowProps(
}
return {
title: translate('AlbumCount'),
title: 'Album Count',
iconName: icons.CIRCLE,
label: albums,
};
@ -168,7 +124,7 @@ function getInfoRowProps(
if (name === 'path') {
return {
title: translate('Path'),
title: 'Path',
iconName: icons.FOLDER,
label: props.path,
};
@ -176,13 +132,31 @@ function getInfoRowProps(
if (name === 'sizeOnDisk') {
return {
title: translate('SizeOnDisk'),
title: 'Size on Disk',
iconName: icons.DRIVE,
label: formatBytes(props.sizeOnDisk),
};
}
}
return null;
interface ArtistIndexOverviewInfoProps {
height: number;
showMonitored: boolean;
showQualityProfile: boolean;
showLastAlbum: boolean;
showAdded: boolean;
showAlbumCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
monitored: boolean;
nextAlbum?: Album;
qualityProfile: object;
lastAlbum?: Album;
added?: string;
albumCount: number;
path: string;
sizeOnDisk?: number;
sortKey: string;
}
function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
@ -201,8 +175,6 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
const { name, showProp, valueProp } = row;
const isVisible =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(7053)
props[valueProp] != null && (props[showProp] || props.sortKey === name);
return {
@ -247,10 +219,6 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
const infoRowProps = getInfoRowProps(row, props, uiSettings);
if (infoRowProps == null) {
return null;
}
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
})}
</div>

View file

@ -1,12 +1,11 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './ArtistIndexOverviewInfoRow.css';
interface ArtistIndexOverviewInfoRowProps {
title?: string;
iconName?: IconDefinition;
label: string | null;
iconName: object;
label: string;
}
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {

View file

@ -1,5 +1,5 @@
import { throttle } from 'lodash';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import Artist from 'Artist/Artist';
@ -33,11 +33,11 @@ interface RowItemData {
interface ArtistIndexOverviewsProps {
items: Artist[];
sortKey: string;
sortKey?: string;
sortDirection?: string;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: RefObject<HTMLElement>;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
const { size: posterSize, detailedProgressBar } = useSelector(
selectOverviewOptions
);
const listRef = useRef<List>(null);
const listRef: React.MutableRefObject<List> = useRef();
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -136,8 +136,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -146,7 +146,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current?.scrollTo(scrollTop);
listRef.current.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -175,8 +175,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
scrollTop += offset;
}
listRef.current?.scrollTo(scrollTop);
scrollerRef.current?.scrollTo(0, scrollTop);
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

View file

@ -60,7 +60,7 @@ function ArtistIndexOverviewOptionsModalContent(
const dispatch = useDispatch();
const onOverviewOptionChange = useCallback(
({ name, value }: { name: string; value: unknown }) => {
({ name, value }) => {
dispatch(setArtistOverviewOption({ [name]: value }));
},
[dispatch]

View file

@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
</div>
) : null}
{showQualityProfile && !!qualityProfile?.name ? (
{showQualityProfile ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>

View file

@ -1,9 +1,8 @@
import { throttle } from 'lodash';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster';
import useMeasure from 'Helpers/Hooks/useMeasure';
@ -22,7 +21,7 @@ const columnPaddingSmallScreen = parseInt(
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
const ADDITIONAL_COLUMN_COUNT = {
small: 3,
medium: 2,
large: 1,
@ -42,17 +41,17 @@ interface CellItemData {
interface ArtistIndexPostersProps {
items: Artist[];
sortKey: string;
sortKey?: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: RefObject<HTMLElement>;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const artistIndexSelector = createSelector(
(state: AppState) => state.artistIndex.posterOptions,
(state) => state.artistIndex.posterOptions,
(posterOptions) => {
return {
posterOptions,
@ -109,7 +108,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
} = props;
const { posterOptions } = useSelector(artistIndexSelector);
const ref = useRef<Grid>(null);
const ref: React.MutableRefObject<Grid> = useRef();
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -232,8 +231,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
}, [isSmallScreen, size, scrollerRef, bounds]);
useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -242,7 +241,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -265,8 +264,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
const scrollTop = rowIndex * rowHeight + padding;
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current?.scrollTo(0, scrollTop);
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current.scrollTo(0, scrollTop);
}
}
}, [

View file

@ -59,7 +59,7 @@ function ArtistIndexPosterOptionsModalContent(
const dispatch = useDispatch();
const onPosterOptionChange = useCallback(
({ name, value }: { name: string; value: unknown }) => {
({ name, value }) => {
dispatch(setArtistPosterOption({ [name]: value }));
},
[dispatch]

View file

@ -1,7 +1,6 @@
import _ from 'lodash';
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Statistics } from 'Album/Album';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
@ -57,8 +56,8 @@ function AlbumDetails(props: AlbumDetailsProps) {
disambiguation,
albumType,
monitored,
statistics = {} as Statistics,
isSaving = false,
statistics,
isSaving,
} = album;
return (

View file

@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps {
artistId: number;
albumId: number;
title: string;
disambiguation?: string;
disambiguation: string;
albumType: string;
monitored: boolean;
statistics: Statistics;

View file

@ -33,7 +33,7 @@ function ChangeMonitoringModalContent(
const [monitor, setMonitor] = useState(NO_CHANGE);
const onInputChange = useCallback(
({ value }: { value: string }) => {
({ value }) => {
setMonitor(value);
},
[setMonitor]

View file

@ -1,4 +1,4 @@
import React, { SyntheticEvent, useCallback } from 'react';
import React, { useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
@ -15,9 +15,8 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
const isSelected = selectState.selectedState[artistId];
const onSelectPress = useCallback(
(event: SyntheticEvent) => {
const nativeEvent = event.nativeEvent as PointerEvent;
const shiftKey = nativeEvent.shiftKey;
(event) => {
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',

View file

@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
interface ArtistIndexSelectAllButtonProps {
label: string;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent<never>;
overflowComponent: React.FunctionComponent;
}
function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {

View file

@ -24,14 +24,6 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal';
import TagsModal from './Tags/TagsModal';
import styles from './ArtistIndexSelectFooter.css';
interface SavePayload {
monitored?: boolean;
qualityProfileId?: number;
metadataProfileId?: number;
rootFolderPath?: string;
moveFiles?: boolean;
}
const artistEditorSelector = createSelector(
(state: AppState) => state.artist,
(artist) => {
@ -87,7 +79,7 @@ function ArtistIndexSelectFooter() {
}, [setIsEditModalOpen]);
const onSavePress = useCallback(
(payload: SavePayload) => {
(payload) => {
setIsSavingArtist(true);
setIsEditModalOpen(false);
@ -126,7 +118,7 @@ function ArtistIndexSelectFooter() {
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
(tags: number[], applyTags: string) => {
(tags, applyTags) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);

View file

@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps {
label: string;
iconName: IconDefinition;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent<never>;
overflowComponent: React.FunctionComponent;
onPress: () => void;
}

View file

@ -28,15 +28,9 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) {
const dispatch = useDispatch();
const artistNames = useMemo(() => {
const artists = artistIds.reduce((acc: Artist[], id) => {
const a = allArtists.find((a) => a.id === id);
if (a) {
acc.push(a);
}
return acc;
}, []);
const artists = artistIds.map((id) => {
return allArtists.find((a) => a.id === id);
});
const sorted = orderBy(artists, ['sortName']);

View file

@ -15,7 +15,6 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './DeleteArtistModalContent.css';
@ -38,16 +37,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) {
const [deleteFiles, setDeleteFiles] = useState(false);
const artists = useMemo((): Artist[] => {
const artistList = artistIds.map((id) => {
const artists = useMemo(() => {
const artists = artistIds.map((id) => {
return allArtists.find((a) => a.id === id);
}) as Artist[];
});
return orderBy(artistList, ['sortName']);
return orderBy(artists, ['sortName']);
}, [artistIds, allArtists]);
const onDeleteFilesChange = useCallback(
({ value }: CheckInputChanged) => {
({ value }) => {
setDeleteFiles(value);
},
[setDeleteFiles]

View file

@ -35,7 +35,7 @@ const monitoredOptions = [
get value() {
return translate('NoChange');
},
isDisabled: true,
disabled: true,
},
{
key: 'monitored',
@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const save = useCallback(
(moveFiles: boolean) => {
(moveFiles) => {
let hasChanges = false;
const payload: SavePayload = {};
@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
({ name, value }) => {
switch (name) {
case 'monitored':
setMonitored(value);

View file

@ -1,7 +1,6 @@
import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tag } from 'App/State/TagsAppState';
import Artist from 'Artist/Artist';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@ -29,7 +28,7 @@ function TagsModalContent(props: TagsModalContentProps) {
const { artistIds, onModalClose, onApplyTagsPress } = props;
const allArtists: Artist[] = useSelector(createAllArtistSelector());
const tagList: Tag[] = useSelector(createTagsSelector());
const tagList = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
@ -49,14 +48,14 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [artistIds, allArtists]);
const onTagsChange = useCallback(
({ value }: { value: number[] }) => {
({ value }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
({ value }: { value: string }) => {
({ value }) => {
setApplyTags(value);
},
[setApplyTags]

View file

@ -67,7 +67,6 @@
flex: 1 0 125px;
}
.monitorNewItems,
.nextAlbum,
.lastAlbum,
.added,

View file

@ -14,7 +14,6 @@ interface CssExports {
'lastAlbum': string;
'link': string;
'metadataProfileId': string;
'monitorNewItems': string;
'nextAlbum': string;
'overlayTitle': string;
'path': string;

View file

@ -23,9 +23,7 @@ import Column from 'Components/Table/Column';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { SelectStateInputProps } from 'typings/props';
import formatBytes from 'Utilities/Number/formatBytes';
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import AlbumsCell from './AlbumsCell';
import hasGrowableColumns from './hasGrowableColumns';
@ -58,7 +56,6 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
monitored,
status,
path,
monitorNewItems,
nextAlbum,
lastAlbum,
added,
@ -129,7 +126,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
}, [setIsDeleteArtistModalOpen]);
const onSelectedChange = useCallback(
({ id, value, shiftKey }: SelectStateInputProps) => {
({ id, value, shiftKey }) => {
selectDispatch({
type: 'toggleSelected',
id,
@ -220,7 +217,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'qualityProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{qualityProfile?.name ?? ''}
{qualityProfile.name}
</VirtualTableRowCell>
);
}
if (name === 'qualityProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{qualityProfile.name}
</VirtualTableRowCell>
);
}
@ -228,15 +233,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'metadataProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{metadataProfile?.name ?? ''}
</VirtualTableRowCell>
);
}
if (name === 'monitorNewItems') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{translate(firstCharToUpper(monitorNewItems))}
{metadataProfile.name}
</VirtualTableRowCell>
);
}
@ -255,7 +252,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
}
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{translate('None')}
None
</VirtualTableRowCell>
);
}
@ -274,15 +271,13 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
}
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{translate('None')}
None
</VirtualTableRowCell>
);
}
if (name === 'added') {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
key={name}
className={styles[name]}
@ -333,7 +328,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'path') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<span title={path}>{path}</span>
{path}
</VirtualTableRowCell>
);
}

View file

@ -1,9 +1,8 @@
import { throttle } from 'lodash';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
@ -31,17 +30,17 @@ interface RowItemData {
interface ArtistIndexTableProps {
items: Artist[];
sortKey: string;
sortKey?: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: RefObject<HTMLElement>;
scrollerRef: React.MutableRefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const columnsSelector = createSelector(
(state: AppState) => state.artistIndex.columns,
(state) => state.artistIndex.columns,
(columns) => columns
);
@ -94,7 +93,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions);
const listRef = useRef<List<RowItemData>>(null);
const listRef: React.MutableRefObject<List> = useRef();
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth;
@ -105,7 +104,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
}, [showBanners]);
useEffect(() => {
const current = scrollerRef?.current as HTMLElement;
const current = scrollerRef.current as HTMLElement;
if (isSmallScreen) {
setSize({
@ -129,8 +128,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -139,7 +138,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current?.scrollTo(scrollTop);
listRef.current.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -168,8 +167,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
scrollTop += offset;
}
listRef.current?.scrollTo(scrollTop);
scrollerRef?.current?.scrollTo(0, scrollTop);
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

View file

@ -31,7 +31,6 @@
flex: 1 0 125px;
}
.monitorNewItems,
.nextAlbum,
.lastAlbum,
.added,

View file

@ -11,7 +11,6 @@ interface CssExports {
'lastAlbum': string;
'latestAlbum': string;
'metadataProfileId': string;
'monitorNewItems': string;
'nextAlbum': string;
'path': string;
'qualityProfileId': string;

View file

@ -15,7 +15,6 @@ import {
setArtistSort,
setArtistTableOption,
} from 'Store/Actions/artistIndexActions';
import { CheckInputChanged } from 'typings/inputs';
import hasGrowableColumns from './hasGrowableColumns';
import styles from './ArtistIndexTableHeader.css';
@ -33,21 +32,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
const [selectState, selectDispatch] = useSelect();
const onSortPress = useCallback(
(value: string) => {
(value) => {
dispatch(setArtistSort({ sortKey: value }));
},
[dispatch]
);
const onTableOptionChange = useCallback(
(payload: unknown) => {
(payload) => {
dispatch(setArtistTableOption(payload));
},
[dispatch]
);
const onSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
({ value }) => {
selectDispatch({
type: value ? 'selectAll' : 'unselectAll',
});
@ -95,8 +94,6 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
<VirtualTableHeaderCell
key={name}
className={classNames(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
styles[name],
name === 'sortName' && showBanners && styles.banner,
name === 'sortName' &&

View file

@ -4,7 +4,6 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import selectTableOptions from './selectTableOptions';
@ -20,7 +19,7 @@ function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) {
const { showBanners, showSearchAction } = tableOptions;
const onTableOptionChangeWrapper = useCallback(
({ name, value }: CheckInputChanged) => {
({ name, value }) => {
onTableOptionChange({
tableOptions: {
...tableOptions,

View file

@ -1,6 +1,5 @@
import { createSelector } from 'reselect';
import Artist from 'Artist/Artist';
import Command from 'Commands/Command';
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
@ -13,21 +12,25 @@ function createArtistIndexItemSelector(artistId: number) {
createArtistQualityProfileSelector(artistId),
createArtistMetadataProfileSelector(artistId),
createExecutingCommandsSelector(),
(
artist: Artist,
qualityProfile,
metadataProfile,
executingCommands: Command[]
) => {
(artist: Artist, qualityProfile, metadataProfile, executingCommands) => {
// If an artist is deleted this selector may fire before the parent
// selectors, which will result in an undefined artist, if that happens
// we want to return early here and again in the render function to avoid
// trying to show an artist that has no information available.
if (!artist) {
return {};
}
const isRefreshingArtist = executingCommands.some((command) => {
return (
command.name === REFRESH_ARTIST && command.body.artistId === artistId
command.name === REFRESH_ARTIST && command.body.artistId === artist.id
);
});
const isSearchingArtist = executingCommands.some((command) => {
return (
command.name === ARTIST_SEARCH && command.body.artistId === artistId
command.name === ARTIST_SEARCH && command.body.artistId === artist.id
);
});

View file

@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_EXTRA_LARGE}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>

View file

@ -1,54 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setCalendarFilter } from 'Store/Actions/calendarActions';
function createCalendarSelector() {
return createSelector(
(state: AppState) => state.calendar.items,
(calendar) => {
return calendar;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.calendar.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface CalendarFilterModalProps {
isOpen: boolean;
}
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setCalendarFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View file

@ -14,7 +14,6 @@ import { align, icons } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import CalendarConnector from './CalendarConnector';
import CalendarFilterModal from './CalendarFilterModal';
import CalendarLinkModal from './iCal/CalendarLinkModal';
import LegendConnector from './Legend/LegendConnector';
import CalendarOptionsModal from './Options/CalendarOptionsModal';
@ -79,7 +78,6 @@ class CalendarPage extends Component {
const {
selectedFilterKey,
filters,
customFilters,
hasArtist,
artistError,
artistIsFetching,
@ -139,8 +137,7 @@ class CalendarPage extends Component {
isDisabled={!hasArtist}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={CalendarFilterModal}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@ -207,7 +204,6 @@ class CalendarPage extends Component {
CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasArtist: PropTypes.bool.isRequired,
artistError: PropTypes.object,
artistIsFetching: PropTypes.bool.isRequired,

View file

@ -6,7 +6,6 @@ import withCurrentPage from 'Components/withCurrentPage';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@ -60,7 +59,6 @@ function createMapStateToProps() {
return createSelector(
(state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters,
createCustomFiltersSelector('calendar'),
createArtistCountSelector(),
createUISettingsSelector(),
createMissingAlbumIdsSelector(),
@ -69,7 +67,6 @@ function createMapStateToProps() {
(
selectedFilterKey,
filters,
customFilters,
artistCount,
uiSettings,
missingAlbumIds,
@ -79,7 +76,6 @@ function createMapStateToProps() {
return {
selectedFilterKey,
filters,
customFilters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasArtist: !!artistCount.count,
artistError: artistCount.error,

View file

@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
type={inputTypes.TAG}
name="tags"
value={tags}
helpText={translate('ICalTagsArtistHelpText')}
helpText={translate('TagsHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>

Some files were not shown because too many files have changed in this diff Show more