diff --git a/.devcontainer/Lidarr.code-workspace b/.devcontainer/Lidarr.code-workspace deleted file mode 100644 index a46158e44..000000000 --- a/.devcontainer/Lidarr.code-workspace +++ /dev/null @@ -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": {} -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index d0fa03d5f..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -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"] - } - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 491815370..31f001e52 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -60,7 +60,6 @@ body: - Master - Develop - Nightly - - Plugins (experimental) - Other (This issue will be closed) validations: required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f33a02cd1..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -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 diff --git a/.github/label-actions.yml b/.github/label-actions.yml deleted file mode 100644 index 3979401b1..000000000 --- a/.github/label-actions.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Configuration for Label Actions - https://github.com/dessant/label-actions - -'Type: Support': - comment: > - :wave: @{issue-author}, we use the issue tracker exclusively - for bug reports and feature requests. However, this issue appears - to be a support request. Please hop over onto our [Discord](https://lidarr.audio/discord). - close: true - close-reason: 'not planned' - -'Status: Logs Needed': - comment: > - :wave: @{issue-author}, In order to help you further we'll need to see logs. - You'll need to enable trace logging and replicate the problem that you encountered. - Guidance on how to enable trace logging can be found in - our [troubleshooting guide](https://wiki.servarr.com/lidarr/troubleshooting#logging-and-log-files). diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml deleted file mode 100644 index a6246a6b3..000000000 --- a/.github/workflows/label-actions.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: 'Label Actions' - -on: - issues: - types: [labeled, unlabeled] - -permissions: - contents: read - issues: write - -jobs: - action: - runs-on: ubuntu-latest - steps: - - uses: dessant/label-actions@v4 - with: - process-only: 'issues' diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 1d50cb1f1..cf38066c5 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -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' diff --git a/.github/workflows/support.yml b/.github/workflows/support.yml new file mode 100644 index 000000000..cdc757378 --- /dev/null +++ b/.github/workflows/support.yml @@ -0,0 +1,32 @@ +name: 'Support requests' + +on: + issues: + types: [labeled, unlabeled, reopened] + +jobs: + support: + runs-on: ubuntu-latest + steps: + - uses: dessant/support-requests@v3 + with: + github-token: ${{ github.token }} + support-label: 'Type: Support' + issue-comment: > + :wave: @{issue-author}, we use the issue tracker exclusively + for bug reports and feature requests. However, this issue appears + to be a support request. Please hop over onto our [Discord](https://lidarr.audio/discord). + close-issue: true + close-reason: 'not planned' + lock-issue: false + - uses: dessant/support-requests@v3 + with: + github-token: ${{ github.token }} + support-label: 'Status: Logs Needed' + issue-comment: > + :wave: @{issue-author}, In order to help you further we'll need to see logs. + You'll need to enable trace logging and replicate the problem that you encountered. + Guidance on how to enable trace logging can be found in + our [troubleshooting guide](https://wiki.servarr.com/lidarr/troubleshooting#logging-and-log-files). + close-issue: false + lock-issue: false diff --git a/.gitignore b/.gitignore index a5d6bb7c8..d2dc01467 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 7a36fefe1..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "esbenp.prettier-vscode", - "ms-dotnettools.csdevkit", - "ms-vscode-remote.remote-containers" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 74b8d418b..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -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" - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 4b3b00b89..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -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" - } - ] -} diff --git a/README.md b/README.md index 7a6da3158..f5c8cdf84 100644 --- a/README.md +++ b/README.md @@ -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 recovering and rebuilding the cache which is impacting adding artists, library imports, etc. Please follow [GHI 5498](https://github.com/Lidarr/Lidarr/issues/5498) or see Discord for details. - ## Major Features Include: * Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ca3735c94..64b91de8d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,18 +9,18 @@ variables: testsFolder: './_tests' yarnCacheFolder: $(Pipeline.Workspace)/.yarn nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages - majorVersion: '2.14.1' + majorVersion: '1.4.3' 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.413' + 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: diff --git a/distribution/debian/install.sh b/distribution/debian/install.sh deleted file mode 100644 index b71eb20c9..000000000 --- a/distribution/debian/install.sh +++ /dev/null @@ -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 </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 diff --git a/distribution/debian/lidarr.service b/distribution/debian/lidarr.service deleted file mode 100644 index 8ec5b5b1d..000000000 --- a/distribution/debian/lidarr.service +++ /dev/null @@ -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 diff --git a/docs.sh b/docs.sh index a44dc90ce..9cbb02756 100644 --- a/docs.sh +++ b/docs.sh @@ -1,18 +1,13 @@ -#!/bin/bash -set -e - -FRAMEWORK="net6.0" PLATFORM=$1 -ARCHITECTURE="${2:-x64}" if [ "$PLATFORM" = "Windows" ]; then - RUNTIME="win-$ARCHITECTURE" + RUNTIME="win-x64" elif [ "$PLATFORM" = "Linux" ]; then - RUNTIME="linux-$ARCHITECTURE" + RUNTIME="linux-x64" elif [ "$PLATFORM" = "Mac" ]; then - RUNTIME="osx-$ARCHITECTURE" + RUNTIME="osx-x64" else - echo "Platform must be provided as first argument: Windows, Linux or Mac" + echo "Platform must be provided as first arguement: Windows, Linux or Mac" exit 1 fi @@ -26,21 +21,15 @@ slnFile=src/Lidarr.sln platform=Posix -if [ "$PLATFORM" = "Windows" ]; then - application=Lidarr.Console.dll -else - application=Lidarr.dll -fi - dotnet clean $slnFile -c Debug dotnet clean $slnFile -c Release dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids dotnet new tool-manifest -dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 & +dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/lidarr.console.dll" v1 & sleep 45 diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index cc26a2633..603b20a48 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -28,8 +28,7 @@ module.exports = { globals: { expect: false, chai: false, - sinon: false, - JSX: true + sinon: false }, parserOptions: { diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index 8da95337f..edb88e0e7 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -9,7 +9,7 @@ "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll": "explicit" + "source.fixAll": true }, "typescript.preferences.quoteStyle": "single", diff --git a/frontend/babel.config.js b/frontend/babel.config.js index ade9f24a2..5c0d5ecdc 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -2,8 +2,6 @@ const loose = true; module.exports = { plugins: [ - '@babel/plugin-transform-logical-assignment-operators', - // Stage 1 '@babel/plugin-proposal-export-default-from', ['@babel/plugin-transform-optional-chaining', { loose }], diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index d1873380e..e0ec27c27 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -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]' } } }, diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 89db00f8c..f657adf28 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -16,7 +16,6 @@ const mixinsFiles = [ module.exports = { plugins: [ - 'autoprefixer', ['postcss-mixins', { mixinsFiles }], diff --git a/frontend/src/Activity/Blocklist/Blocklist.js b/frontend/src/Activity/Blocklist/Blocklist.js index ab43c106d..6668bee46 100644 --- a/frontend/src/Activity/Blocklist/Blocklist.js +++ b/frontend/src/Activity/Blocklist/Blocklist.js @@ -36,7 +36,6 @@ class Blocklist extends Component { lastToggled: null, selectedState: {}, isConfirmRemoveModalOpen: false, - isConfirmClearModalOpen: false, items: props.items }; } @@ -91,19 +90,6 @@ class Blocklist extends Component { this.setState({ isConfirmRemoveModalOpen: false }); }; - onClearBlocklistPress = () => { - this.setState({ isConfirmClearModalOpen: true }); - }; - - onClearBlocklistConfirmed = () => { - this.props.onClearBlocklistPress(); - this.setState({ isConfirmClearModalOpen: false }); - }; - - onConfirmClearModalClose = () => { - this.setState({ isConfirmClearModalOpen: false }); - }; - // // Render @@ -119,6 +105,7 @@ class Blocklist extends Component { totalRecords, isRemoving, isClearingBlocklistExecuting, + onClearBlocklistPress, ...otherProps } = this.props; @@ -129,8 +116,7 @@ class Blocklist extends Component { allSelected, allUnselected, selectedState, - isConfirmRemoveModalOpen, - isConfirmClearModalOpen + isConfirmRemoveModalOpen } = this.state; const selectedIds = this.getSelectedIds(); @@ -150,9 +136,8 @@ class Blocklist extends Component { @@ -235,16 +220,6 @@ class Blocklist extends Component { onConfirm={this.onRemoveSelectedConfirmed} onCancel={this.onConfirmRemoveModalClose} /> - - ); } diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js index 84aa3e0f2..cc0e69fd5 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.js +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -60,7 +60,6 @@ function HistoryDetails(props) { eventType, sourceTitle, data, - downloadId, shortDateFormat, timeFormat } = props; @@ -73,6 +72,7 @@ function HistoryDetails(props) { nzbInfoUrl, downloadClient, downloadClientName, + downloadId, age, ageHours, ageMinutes, @@ -90,22 +90,20 @@ function HistoryDetails(props) { /> { - indexer ? + !!indexer && : - null + /> } { - releaseGroup ? + !!releaseGroup && : - null + /> } { @@ -121,7 +119,7 @@ function HistoryDetails(props) { nzbInfoUrl ? - {translate('InfoUrl')} + Info URL @@ -141,30 +139,27 @@ function HistoryDetails(props) { } { - downloadId ? + !!downloadId && : - null + /> } { - age || ageHours || ageMinutes ? + !!indexer && : - null + /> } { - publishedDate ? + !!publishedDate && : - null + /> } ); @@ -172,8 +167,7 @@ function HistoryDetails(props) { if (eventType === 'downloadFailed') { const { - message, - indexer + message } = data; return ( @@ -185,29 +179,11 @@ function HistoryDetails(props) { /> { - downloadId ? - : - null - } - - { - indexer ? ( - - ) : null} - - { - message ? + !!message && : - null + /> } ); @@ -229,13 +205,12 @@ function HistoryDetails(props) { /> { - droppedPath ? + !!droppedPath && : - null + /> } { @@ -273,7 +248,7 @@ function HistoryDetails(props) { reasonMessage = 'File was deleted by via UI'; break; case 'MissingFromDisk': - reasonMessage = 'Lidarr was unable to find the file on disk so the file was unlinked from the album/track in the database'; + reasonMessage = 'Lidarr was unable to find the file on disk so it was removed'; break; case 'Upgrade': reasonMessage = 'File was deleted to import an upgrade'; @@ -385,9 +360,9 @@ function HistoryDetails(props) { const { indexer, releaseGroup, - customFormatScore, nzbInfoUrl, downloadClient, + downloadId, age, ageHours, ageMinutes, @@ -402,80 +377,64 @@ function HistoryDetails(props) { /> { - indexer ? + !!indexer && : - null + /> } { - releaseGroup ? + !!releaseGroup && : - null + /> } { - customFormatScore && customFormatScore !== '0' ? - : - null - } - - { - nzbInfoUrl ? + !!nzbInfoUrl && - {translate('InfoUrl')} + Info URL {nzbInfoUrl} - : - null + } { - downloadClient ? + !!downloadClient && : - null + /> } { - downloadId ? + !!downloadId && : - null + /> } { - age || ageHours || ageMinutes ? + !!indexer && : - null + /> } { - publishedDate ? + !!publishedDate && : - null + /> } ); @@ -495,21 +454,11 @@ function HistoryDetails(props) { /> { - downloadId ? - : - null - } - - { - message ? + !!message && : - null + /> } ); @@ -530,7 +479,6 @@ HistoryDetails.propTypes = { eventType: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired, data: PropTypes.object.isRequired, - downloadId: PropTypes.string, shortDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired }; diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.js b/frontend/src/Activity/History/Details/HistoryDetailsModal.js index 5362a2f43..187db9cd4 100644 --- a/frontend/src/Activity/History/Details/HistoryDetailsModal.js +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.js @@ -42,7 +42,6 @@ function HistoryDetailsModal(props) { eventType, sourceTitle, data, - downloadId, isMarkingAsFailed, shortDateFormat, timeFormat, @@ -65,7 +64,6 @@ function HistoryDetailsModal(props) { eventType={eventType} sourceTitle={sourceTitle} data={data} - downloadId={downloadId} shortDateFormat={shortDateFormat} timeFormat={timeFormat} /> @@ -100,7 +98,6 @@ HistoryDetailsModal.propTypes = { eventType: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired, data: PropTypes.object.isRequired, - downloadId: PropTypes.string, isMarkingAsFailed: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired, diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js index d144a5402..5c830ee1a 100644 --- a/frontend/src/Activity/History/History.js +++ b/frontend/src/Activity/History/History.js @@ -15,7 +15,6 @@ import TablePager from 'Components/Table/TablePager'; import { align, icons, kinds } from 'Helpers/Props'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import translate from 'Utilities/String/translate'; -import HistoryFilterModal from './HistoryFilterModal'; import HistoryRowConnector from './HistoryRowConnector'; class History extends Component { @@ -53,7 +52,6 @@ class History extends Component { columns, selectedFilterKey, filters, - customFilters, totalRecords, isArtistFetching, isArtistPopulated, @@ -96,8 +94,7 @@ class History extends Component { alignMenu={align.RIGHT} selectedFilterKey={selectedFilterKey} filters={filters} - customFilters={customFilters} - filterModalConnectorComponent={HistoryFilterModal} + customFilters={[]} onFilterSelect={onFilterSelect} /> @@ -168,9 +165,8 @@ History.propTypes = { error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + selectedFilterKey: PropTypes.string.isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, totalRecords: PropTypes.number, isArtistFetching: PropTypes.bool.isRequired, isArtistPopulated: PropTypes.bool.isRequired, diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js index 2b3354bc5..801aaf0e0 100644 --- a/frontend/src/Activity/History/HistoryConnector.js +++ b/frontend/src/Activity/History/HistoryConnector.js @@ -6,7 +6,6 @@ import withCurrentPage from 'Components/withCurrentPage'; import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions'; import * as historyActions from 'Store/Actions/historyActions'; import { clearTracks, fetchTracks } from 'Store/Actions/trackActions'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; @@ -18,8 +17,7 @@ function createMapStateToProps() { (state) => state.artist, (state) => state.albums, (state) => state.tracks, - createCustomFiltersSelector('history'), - (history, artist, albums, tracks, customFilters) => { + (history, artist, albums, tracks) => { return { isArtistFetching: artist.isFetching, isArtistPopulated: artist.isPopulated, @@ -29,7 +27,6 @@ function createMapStateToProps() { isTracksFetching: tracks.isFetching, isTracksPopulated: tracks.isPopulated, tracksError: tracks.error, - customFilters, ...history }; } diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.js b/frontend/src/Activity/History/HistoryEventTypeCell.js index 937cedd98..cc0495d67 100644 --- a/frontend/src/Activity/History/HistoryEventTypeCell.js +++ b/frontend/src/Activity/History/HistoryEventTypeCell.js @@ -3,10 +3,9 @@ import React from 'react'; import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; import styles from './HistoryEventTypeCell.css'; -function getIconName(eventType, data) { +function getIconName(eventType) { switch (eventType) { case 'grabbed': return icons.DOWNLOADING; @@ -17,7 +16,7 @@ function getIconName(eventType, data) { case 'downloadFailed': return icons.DOWNLOADING; case 'trackFileDeleted': - return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE; + return icons.DELETE; case 'trackFileRenamed': return icons.ORGANIZE; case 'trackFileRetagged': @@ -55,11 +54,11 @@ function getTooltip(eventType, data) { case 'downloadFailed': return 'Album download failed'; case 'trackFileDeleted': - return data.reason === 'MissingFromDisk' ? translate('TrackFileMissingTooltip') : translate('TrackFileDeletedTooltip'); + return 'Track file deleted'; case 'trackFileRenamed': - return translate('TrackFileRenamedTooltip'); + return 'Track file renamed'; case 'trackFileRetagged': - return translate('TrackFileTagsUpdatedTooltip'); + return 'Track file tags updated'; case 'albumImportIncomplete': return 'Files downloaded but not all could be imported'; case 'downloadImported': @@ -72,7 +71,7 @@ function getTooltip(eventType, data) { } function HistoryEventTypeCell({ eventType, data }) { - const iconName = getIconName(eventType, data); + const iconName = getIconName(eventType); const iconKind = getIconKind(eventType); const tooltip = getTooltip(eventType, data); diff --git a/frontend/src/Activity/History/HistoryFilterModal.tsx b/frontend/src/Activity/History/HistoryFilterModal.tsx deleted file mode 100644 index f4ad2e57c..000000000 --- a/frontend/src/Activity/History/HistoryFilterModal.tsx +++ /dev/null @@ -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 { setHistoryFilter } from 'Store/Actions/historyActions'; - -function createHistorySelector() { - return createSelector( - (state: AppState) => state.history.items, - (queueItems) => { - return queueItems; - } - ); -} - -function createFilterBuilderPropsSelector() { - return createSelector( - (state: AppState) => state.history.filterBuilderProps, - (filterBuilderProps) => { - return filterBuilderProps; - } - ); -} - -interface HistoryFilterModalProps { - isOpen: boolean; -} - -export default function HistoryFilterModal(props: HistoryFilterModalProps) { - const sectionItems = useSelector(createHistorySelector()); - const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'history'; - - const dispatch = useDispatch(); - - const dispatchSetFilter = useCallback( - (payload: unknown) => { - dispatch(setHistoryFilter(payload)); - }, - [dispatch] - ); - - return ( - - ); -} diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js index 9f2da78d0..23dea5416 100644 --- a/frontend/src/Activity/History/HistoryRow.js +++ b/frontend/src/Activity/History/HistoryRow.js @@ -65,7 +65,6 @@ class HistoryRow extends Component { sourceTitle, date, data, - downloadId, isMarkingAsFailed, columns, shortDateFormat, @@ -245,7 +244,6 @@ class HistoryRow extends Component { eventType={eventType} sourceTitle={sourceTitle} data={data} - downloadId={downloadId} isMarkingAsFailed={isMarkingAsFailed} shortDateFormat={shortDateFormat} timeFormat={timeFormat} @@ -271,7 +269,6 @@ HistoryRow.propTypes = { sourceTitle: PropTypes.string.isRequired, date: PropTypes.string.isRequired, data: PropTypes.object.isRequired, - downloadId: PropTypes.string, isMarkingAsFailed: PropTypes.bool, markAsFailedError: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 0efc29f21..b73ce0ad7 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import FilterMenu from 'Components/Menu/FilterMenu'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; @@ -22,10 +21,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; -import QueueFilterModal from './QueueFilterModal'; import QueueOptionsConnector from './QueueOptionsConnector'; import QueueRowConnector from './QueueRowConnector'; -import RemoveQueueItemModal from './RemoveQueueItemModal'; +import RemoveQueueItemsModal from './RemoveQueueItemsModal'; class Queue extends Component { @@ -157,16 +155,11 @@ class Queue extends Component { isAlbumsPopulated, albumsError, columns, - selectedFilterKey, - filters, - customFilters, - count, totalRecords, isGrabbing, isRemoving, isRefreshMonitoredDownloadsExecuting, onRefreshPress, - onFilterSelect, ...otherProps } = this.props; @@ -229,15 +222,6 @@ class Queue extends Component { iconName={icons.TABLE} /> - - @@ -259,11 +243,7 @@ class Queue extends Component { { isAllPopulated && !hasError && !items.length ? - { - selectedFilterKey !== 'all' && count > 0 ? - translate('QueueFilterHasNoItems') : - translate('QueueIsEmpty') - } + {translate('QueueIsEmpty')} : null } @@ -309,16 +289,9 @@ class Queue extends Component { } - { - const item = items.find((i) => i.id === id); - - return !!(item && item.downloadClientHasPostImportCategory); - }) - )} canIgnore={isConfirmRemoveModalOpen && ( selectedIds.every((id) => { const item = items.find((i) => i.id === id); @@ -326,17 +299,6 @@ class Queue extends Component { return !!(item && item.artistId && item.albumId); }) )} - pending={isConfirmRemoveModalOpen && ( - selectedIds.every((id) => { - const item = items.find((i) => i.id === id); - - if (!item) { - return false; - } - - return item.status === 'delay' || item.status === 'downloadClientUnavailable'; - }) - )} onRemovePress={this.onRemoveSelectedConfirmed} onModalClose={this.onConfirmRemoveModalClose} /> @@ -356,22 +318,13 @@ Queue.propTypes = { isAlbumsPopulated: PropTypes.bool.isRequired, albumsError: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - count: PropTypes.number.isRequired, totalRecords: PropTypes.number, isGrabbing: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired, isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired, onRefreshPress: PropTypes.func.isRequired, onGrabSelectedPress: PropTypes.func.isRequired, - onRemoveSelectedPress: PropTypes.func.isRequired, - onFilterSelect: PropTypes.func.isRequired -}; - -Queue.defaultProps = { - count: 0 + onRemoveSelectedPress: PropTypes.func.isRequired }; export default Queue; diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js index fc0bb4699..1053f6f26 100644 --- a/frontend/src/Activity/Queue/QueueConnector.js +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -7,7 +7,6 @@ import withCurrentPage from 'Components/withCurrentPage'; import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions'; import { executeCommand } from 'Store/Actions/commandActions'; import * as queueActions from 'Store/Actions/queueActions'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; @@ -20,18 +19,14 @@ function createMapStateToProps() { (state) => state.albums, (state) => state.queue.options, (state) => state.queue.paged, - (state) => state.queue.status.item, - createCustomFiltersSelector('queue'), createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS), - (artist, albums, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => { + (artist, albums, options, queue, isRefreshMonitoredDownloadsExecuting) => { return { - count: options.includeUnknownArtistItems ? status.totalCount : status.count, isArtistFetching: artist.isFetching, isArtistPopulated: artist.isPopulated, isAlbumsFetching: albums.isFetching, isAlbumsPopulated: albums.isPopulated, albumsError: albums.error, - customFilters, isRefreshMonitoredDownloadsExecuting, ...options, ...queue @@ -130,10 +125,6 @@ class QueueConnector extends Component { this.props.setQueueSort({ sortKey }); }; - onFilterSelect = (selectedFilterKey) => { - this.props.setQueueFilter({ selectedFilterKey }); - }; - onTableOptionChange = (payload) => { this.props.setQueueTableOption(payload); @@ -168,7 +159,6 @@ class QueueConnector extends Component { onLastPagePress={this.onLastPagePress} onPageSelect={this.onPageSelect} onSortPress={this.onSortPress} - onFilterSelect={this.onFilterSelect} onTableOptionChange={this.onTableOptionChange} onRefreshPress={this.onRefreshPress} onGrabSelectedPress={this.onGrabSelectedPress} @@ -191,7 +181,6 @@ QueueConnector.propTypes = { gotoQueueLastPage: PropTypes.func.isRequired, gotoQueuePage: PropTypes.func.isRequired, setQueueSort: PropTypes.func.isRequired, - setQueueFilter: PropTypes.func.isRequired, setQueueTableOption: PropTypes.func.isRequired, clearQueue: PropTypes.func.isRequired, grabQueueItems: PropTypes.func.isRequired, diff --git a/frontend/src/Activity/Queue/QueueFilterModal.tsx b/frontend/src/Activity/Queue/QueueFilterModal.tsx deleted file mode 100644 index 3fce6c166..000000000 --- a/frontend/src/Activity/Queue/QueueFilterModal.tsx +++ /dev/null @@ -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 { setQueueFilter } from 'Store/Actions/queueActions'; - -function createQueueSelector() { - return createSelector( - (state: AppState) => state.queue.paged.items, - (queueItems) => { - return queueItems; - } - ); -} - -function createFilterBuilderPropsSelector() { - return createSelector( - (state: AppState) => state.queue.paged.filterBuilderProps, - (filterBuilderProps) => { - return filterBuilderProps; - } - ); -} - -interface QueueFilterModalProps { - isOpen: boolean; -} - -export default function QueueFilterModal(props: QueueFilterModalProps) { - const sectionItems = useSelector(createQueueSelector()); - const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'queue'; - - const dispatch = useDispatch(); - - const dispatchSetFilter = useCallback( - (payload: unknown) => { - dispatch(setQueueFilter(payload)); - }, - [dispatch] - ); - - return ( - - ); -} diff --git a/frontend/src/Activity/Queue/QueueRow.css b/frontend/src/Activity/Queue/QueueRow.css index 2a0df3595..6636f2f9d 100644 --- a/frontend/src/Activity/Queue/QueueRow.css +++ b/frontend/src/Activity/Queue/QueueRow.css @@ -26,5 +26,4 @@ composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 90px; - text-align: right; } diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index d0f1fbacf..08634d9f7 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -98,10 +98,8 @@ class QueueRow extends Component { indexer, outputPath, downloadClient, - downloadClientHasPostImportCategory, downloadForced, estimatedCompletionTime, - added, timeleft, size, sizeleft, @@ -330,15 +328,6 @@ class QueueRow extends Component { ); } - if (name === 'added') { - return ( - - ); - } - if (name === 'actions') { return ( @@ -434,10 +421,8 @@ QueueRow.propTypes = { indexer: PropTypes.string, outputPath: PropTypes.string, downloadClient: PropTypes.string, - downloadClientHasPostImportCategory: PropTypes.bool, downloadForced: PropTypes.bool.isRequired, estimatedCompletionTime: PropTypes.string, - added: PropTypes.string, timeleft: PropTypes.string, size: PropTypes.number, sizeleft: PropTypes.number, diff --git a/frontend/src/Activity/Queue/QueueStatusCell.js b/frontend/src/Activity/Queue/QueueStatusCell.js index d4dcdfeee..6d5caf6f9 100644 --- a/frontend/src/Activity/Queue/QueueStatusCell.js +++ b/frontend/src/Activity/Queue/QueueStatusCell.js @@ -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'; } } diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.js b/frontend/src/Activity/Queue/RemoveQueueItemModal.js new file mode 100644 index 000000000..d9e4dd7f6 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.js @@ -0,0 +1,171 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +class RemoveQueueItemModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + removeFromClient: true, + blocklist: false, + skipRedownload: false + }; + } + + // + // Control + + resetState = function() { + this.setState({ + removeFromClient: true, + blocklist: false, + skipRedownload: false + }); + }; + + // + // Listeners + + onRemoveFromClientChange = ({ value }) => { + this.setState({ removeFromClient: value }); + }; + + onBlocklistChange = ({ value }) => { + this.setState({ blocklist: value }); + }; + + onSkipRedownloadChange = ({ value }) => { + this.setState({ skipRedownload: value }); + }; + + onRemoveConfirmed = () => { + const state = this.state; + + this.resetState(); + this.props.onRemovePress(state); + }; + + onModalClose = () => { + this.resetState(); + this.props.onModalClose(); + }; + + // + // Render + + render() { + const { + isOpen, + sourceTitle, + canIgnore + } = this.props; + + const { removeFromClient, blocklist, skipRedownload } = this.state; + + return ( + + + + Remove - {sourceTitle} + + + +
+ Are you sure you want to remove '{sourceTitle}' from the queue? +
+ + + + {translate('RemoveFromDownloadClient')} + + + + + + + + {translate('BlocklistRelease')} + + + + + + { + blocklist && + + + {translate('SkipRedownload')} + + + + } + +
+ + + + + + +
+
+ ); + } +} + +RemoveQueueItemModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + sourceTitle: PropTypes.string.isRequired, + canIgnore: PropTypes.bool.isRequired, + onRemovePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx deleted file mode 100644 index f25bb0d1b..000000000 --- a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import Modal from 'Components/Modal/Modal'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './RemoveQueueItemModal.css'; - -interface RemovePressProps { - removeFromClient: boolean; - changeCategory: boolean; - blocklist: boolean; - skipRedownload: boolean; -} - -interface RemoveQueueItemModalProps { - isOpen: boolean; - sourceTitle: string; - canChangeCategory: boolean; - canIgnore: boolean; - isPending: boolean; - selectedCount?: number; - onRemovePress(props: RemovePressProps): void; - onModalClose: () => void; -} - -type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore'; -type BlocklistMethod = - | 'doNotBlocklist' - | 'blocklistAndSearch' - | 'blocklistOnly'; - -function RemoveQueueItemModal(props: RemoveQueueItemModalProps) { - const { - isOpen, - sourceTitle, - canIgnore, - canChangeCategory, - isPending, - selectedCount, - onRemovePress, - onModalClose, - } = props; - - const multipleSelected = selectedCount && selectedCount > 1; - - const [removalMethod, setRemovalMethod] = - useState('removeFromClient'); - const [blocklistMethod, setBlocklistMethod] = - useState('doNotBlocklist'); - - const { title, message } = useMemo(() => { - if (!selectedCount) { - return { - title: translate('RemoveQueueItem', { sourceTitle }), - message: translate('RemoveQueueItemConfirmation', { sourceTitle }), - }; - } - - if (selectedCount === 1) { - return { - title: translate('RemoveSelectedItem'), - message: translate('RemoveSelectedItemQueueMessageText'), - }; - } - - return { - title: translate('RemoveSelectedItems'), - message: translate('RemoveSelectedItemsQueueMessageText', { - selectedCount, - }), - }; - }, [sourceTitle, selectedCount]); - - const removalMethodOptions = useMemo(() => { - return [ - { - key: 'removeFromClient', - value: translate('RemoveFromDownloadClient'), - hint: multipleSelected - ? translate('RemoveMultipleFromDownloadClientHint') - : translate('RemoveFromDownloadClientHint'), - }, - { - key: 'changeCategory', - value: translate('ChangeCategory'), - isDisabled: !canChangeCategory, - hint: multipleSelected - ? translate('ChangeCategoryMultipleHint') - : translate('ChangeCategoryHint'), - }, - { - key: 'ignore', - value: multipleSelected - ? translate('IgnoreDownloads') - : translate('IgnoreDownload'), - isDisabled: !canIgnore, - hint: multipleSelected - ? translate('IgnoreDownloadsHint') - : translate('IgnoreDownloadHint'), - }, - ]; - }, [canChangeCategory, canIgnore, multipleSelected]); - - const blocklistMethodOptions = useMemo(() => { - return [ - { - key: 'doNotBlocklist', - value: translate('DoNotBlocklist'), - hint: translate('DoNotBlocklistHint'), - }, - { - key: 'blocklistAndSearch', - value: translate('BlocklistAndSearch'), - isDisabled: isPending, - hint: multipleSelected - ? translate('BlocklistAndSearchMultipleHint') - : translate('BlocklistAndSearchHint'), - }, - { - key: 'blocklistOnly', - value: translate('BlocklistOnly'), - hint: multipleSelected - ? translate('BlocklistMultipleOnlyHint') - : translate('BlocklistOnlyHint'), - }, - ]; - }, [isPending, multipleSelected]); - - const handleRemovalMethodChange = useCallback( - ({ value }: { value: RemovalMethod }) => { - setRemovalMethod(value); - }, - [setRemovalMethod] - ); - - const handleBlocklistMethodChange = useCallback( - ({ value }: { value: BlocklistMethod }) => { - setBlocklistMethod(value); - }, - [setBlocklistMethod] - ); - - const handleConfirmRemove = useCallback(() => { - onRemovePress({ - removeFromClient: removalMethod === 'removeFromClient', - changeCategory: removalMethod === 'changeCategory', - blocklist: blocklistMethod !== 'doNotBlocklist', - skipRedownload: blocklistMethod === 'blocklistOnly', - }); - - setRemovalMethod('removeFromClient'); - setBlocklistMethod('doNotBlocklist'); - }, [ - removalMethod, - blocklistMethod, - setRemovalMethod, - setBlocklistMethod, - onRemovePress, - ]); - - const handleModalClose = useCallback(() => { - setRemovalMethod('removeFromClient'); - setBlocklistMethod('doNotBlocklist'); - - onModalClose(); - }, [setRemovalMethod, setBlocklistMethod, onModalClose]); - - return ( - - - {title} - - -
{message}
- - {isPending ? null : ( - - {translate('RemoveQueueItemRemovalMethod')} - - - - )} - - - - {multipleSelected - ? translate('BlocklistReleases') - : translate('BlocklistRelease')} - - - - -
- - - - - - -
-
- ); -} - -export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.css b/frontend/src/Activity/Queue/RemoveQueueItemsModal.css similarity index 100% rename from frontend/src/Activity/Queue/RemoveQueueItemModal.css rename to frontend/src/Activity/Queue/RemoveQueueItemsModal.css diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.css.d.ts b/frontend/src/Activity/Queue/RemoveQueueItemsModal.css.d.ts similarity index 100% rename from frontend/src/Activity/Queue/RemoveQueueItemModal.css.d.ts rename to frontend/src/Activity/Queue/RemoveQueueItemsModal.css.d.ts diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js new file mode 100644 index 000000000..3b9164e68 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js @@ -0,0 +1,172 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './RemoveQueueItemsModal.css'; + +class RemoveQueueItemsModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + removeFromClient: true, + blocklist: false, + skipRedownload: false + }; + } + + // + // Control + + resetState = function() { + this.setState({ + removeFromClient: true, + blocklist: false, + skipRedownload: false + }); + }; + + // + // Listeners + + onRemoveFromClientChange = ({ value }) => { + this.setState({ removeFromClient: value }); + }; + + onBlocklistChange = ({ value }) => { + this.setState({ blocklist: value }); + }; + + onSkipRedownloadChange = ({ value }) => { + this.setState({ skipRedownload: value }); + }; + + onRemoveConfirmed = () => { + const state = this.state; + + this.resetState(); + this.props.onRemovePress(state); + }; + + onModalClose = () => { + this.resetState(); + this.props.onModalClose(); + }; + + // + // Render + + render() { + const { + isOpen, + selectedCount, + canIgnore + } = this.props; + + const { removeFromClient, blocklist, skipRedownload } = this.state; + + return ( + + + + {selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')} + + + +
+ {selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', [selectedCount]) : translate('RemoveSelectedItemQueueMessageText')} +
+ + + + {translate('RemoveFromDownloadClient')} + + + + + + + + {selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')} + + + + + + { + blocklist && + + + {translate('SkipRedownload')} + + + + } + +
+ + + + + + +
+
+ ); + } +} + +RemoveQueueItemsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + selectedCount: PropTypes.number.isRequired, + canIgnore: PropTypes.bool.isRequired, + onRemovePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default RemoveQueueItemsModal; diff --git a/frontend/src/Activity/Queue/TimeleftCell.js b/frontend/src/Activity/Queue/TimeleftCell.js index b280b5a06..f2aa1390a 100644 --- a/frontend/src/Activity/Queue/TimeleftCell.js +++ b/frontend/src/Activity/Queue/TimeleftCell.js @@ -1,9 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Tooltip from 'Components/Tooltip/Tooltip'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatTime from 'Utilities/Date/formatTime'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; @@ -28,13 +25,11 @@ function TimeleftCell(props) { const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( - - } - tooltip={translate('DelayingDownloadUntil', { date, time })} - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> + + - ); } @@ -44,13 +39,11 @@ function TimeleftCell(props) { const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( - - } - tooltip={translate('RetryingDownloadOn', { date, time })} - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> + + - ); } diff --git a/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js b/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js index cda224e2f..5a837b1eb 100644 --- a/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js +++ b/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js @@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() { ); diff --git a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css deleted file mode 100644 index 7393b9c35..000000000 --- a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css +++ /dev/null @@ -1,5 +0,0 @@ -.message { - composes: alert from '~Components/Alert.css'; - - margin-bottom: 30px; -} diff --git a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js index d53bda8e3..653e313e1 100644 --- a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js +++ b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js @@ -2,17 +2,14 @@ import React from 'react'; import Alert from 'Components/Alert'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; -import styles from './ArtistMonitoringOptionsPopoverContent.css'; function ArtistMonitoringOptionsPopoverContent() { return ( <> - + This is a one time adjustment to set which albums are monitored - - {albumTitle} + + {title}{disambiguation ? ` (${disambiguation})` : ''} ); } diff --git a/frontend/src/Album/Delete/DeleteAlbumModalContent.js b/frontend/src/Album/Delete/DeleteAlbumModalContent.js index 28505ea75..c637c3dd4 100644 --- a/frontend/src/Album/Delete/DeleteAlbumModalContent.js +++ b/frontend/src/Album/Delete/DeleteAlbumModalContent.js @@ -53,13 +53,13 @@ class DeleteAlbumModalContent extends Component { render() { const { title, - statistics = {}, + statistics, onModalClose } = this.props; const { - trackFileCount = 0, - sizeOnDisk = 0 + trackFileCount, + sizeOnDisk } = statistics; const deleteFiles = this.state.deleteFiles; @@ -133,14 +133,14 @@ class DeleteAlbumModalContent extends Component { diff --git a/frontend/src/Album/Details/AlbumDetails.css b/frontend/src/Album/Details/AlbumDetails.css index a676ae574..dd6626c96 100644 --- a/frontend/src/Album/Details/AlbumDetails.css +++ b/frontend/src/Album/Details/AlbumDetails.css @@ -119,10 +119,7 @@ margin: 5px 10px 5px 0; } -.releaseDate, .sizeOnDisk, -.albumType, -.secondaryTypes, .qualityProfileName, .links, .tags { @@ -150,12 +147,6 @@ .headerContent { padding: 15px; } - - .title { - font-weight: 300; - font-size: 30px; - line-height: 30px; - } } @media only screen and (max-width: $breakpointLarge) { diff --git a/frontend/src/Album/Details/AlbumDetails.css.d.ts b/frontend/src/Album/Details/AlbumDetails.css.d.ts index 1d14a0ccf..82ef9f0e0 100644 --- a/frontend/src/Album/Details/AlbumDetails.css.d.ts +++ b/frontend/src/Album/Details/AlbumDetails.css.d.ts @@ -3,7 +3,6 @@ interface CssExports { 'albumNavigationButton': string; 'albumNavigationButtons': string; - 'albumType': string; 'alternateTitlesIconContainer': string; 'backdrop': string; 'backdropOverlay': string; @@ -20,8 +19,6 @@ interface CssExports { 'monitorToggleButton': string; 'overview': string; 'qualityProfileName': string; - 'releaseDate': string; - 'secondaryTypes': string; 'sizeOnDisk': string; 'tags': string; 'title': string; diff --git a/frontend/src/Album/Details/AlbumDetails.js b/frontend/src/Album/Details/AlbumDetails.js index fe007e168..0a6c883a4 100644 --- a/frontend/src/Album/Details/AlbumDetails.js +++ b/frontend/src/Album/Details/AlbumDetails.js @@ -9,7 +9,6 @@ import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector'; import AlbumInteractiveSearchModalConnector from 'Album/Search/AlbumInteractiveSearchModalConnector'; import ArtistGenres from 'Artist/Details/ArtistGenres'; import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal'; -import Alert from 'Components/Alert'; import HeartRating from 'Components/HeartRating'; import Icon from 'Components/Icon'; import Label from 'Components/Label'; @@ -40,7 +39,11 @@ const intermediateFontSize = parseInt(fonts.intermediateFontSize); const lineHeight = parseFloat(fonts.lineHeight); function getFanartUrl(images) { - return _.find(images, { coverType: 'fanart' })?.url; + const fanartImage = _.find(images, { coverType: 'fanart' }); + if (fanartImage) { + // Remove protocol + return fanartImage.url.replace(/^https?:/, ''); + } } function formatDuration(timeSpan) { @@ -192,7 +195,6 @@ class AlbumDetails extends Component { duration, overview, albumType, - secondaryTypes, statistics = {}, monitored, releaseDate, @@ -205,7 +207,6 @@ class AlbumDetails extends Component { isFetching, isPopulated, albumsError, - tracksError, trackFilesError, hasTrackFiles, shortDateFormat, @@ -218,8 +219,8 @@ class AlbumDetails extends Component { } = this.props; const { - trackFileCount = 0, - sizeOnDisk = 0 + trackFileCount, + sizeOnDisk } = statistics; const { @@ -398,11 +399,10 @@ class AlbumDetails extends Component {
{ - duration ? + !!duration && {formatDuration(duration)} - : - null + } -
- - - {moment(releaseDate).format(shortDateFormat)} - -
+ + + + { + moment(releaseDate).format(shortDateFormat) + } + -
- - - {formatBytes(sizeOnDisk)} - -
+ + + + { + formatBytes(sizeOnDisk || 0) + } + } tooltip={ @@ -462,55 +463,32 @@ class AlbumDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > -
- - - {monitored ? translate('Monitored') : translate('Unmonitored')} - -
+ + + + {monitored ? 'Monitored' : 'Unmonitored'} + { - albumType ? + !!albumType && : - null - } + - { - secondaryTypes.length ? - : - null + + {albumType} + + } -
- - - {translate('Links')} - -
+ + + + Links + } tooltip={ @@ -553,38 +530,28 @@ class AlbumDetails extends Component {
{ - !isPopulated && !albumsError && !tracksError && !trackFilesError ? - : - null + !isPopulated && !albumsError && !trackFilesError && + } { - !isFetching && albumsError ? - - {translate('AlbumsLoadError')} - : - null + !isFetching && albumsError && +
+ {translate('LoadingAlbumsFailed')} +
} { - !isFetching && tracksError ? - - {translate('TracksLoadError')} - : - null - } - - { - !isFetching && trackFilesError ? - - {translate('TrackFilesLoadError')} - : - null + !isFetching && trackFilesError && +
+ {translate('LoadingTrackFilesFailed')} +
} { isPopulated && !!media.length &&
+ { media.slice(0).map((medium) => { return ( @@ -602,14 +569,6 @@ class AlbumDetails extends Component {
} - { - isPopulated && !media.length ? - - {translate('NoMediumInformation')} - : - null - } -
-1 ); - const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id })); - const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST }); - const isRenamingArtist = ( - isCommandExecuting(isRenamingArtistCommand) && - isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1 - ); const isFetching = tracks.isFetching || isTrackFilesFetching; const isPopulated = tracks.isPopulated && isTrackFilesPopulated; @@ -86,8 +80,6 @@ function createMapStateToProps() { shortDateFormat: uiSettings.shortDateFormat, artist, isSearching, - isRenamingFiles, - isRenamingArtist, isFetching, isPopulated, tracksError, @@ -121,27 +113,8 @@ class AlbumDetailsConnector extends Component { } componentDidUpdate(prevProps) { - const { - id, - anyReleaseOk, - isRenamingFiles, - isRenamingArtist - } = this.props; - - if ( - (prevProps.isRenamingFiles && !isRenamingFiles) || - (prevProps.isRenamingArtist && !isRenamingArtist) || - !_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) || - (prevProps.anyReleaseOk === false && anyReleaseOk === true) - ) { - this.unpopulate(); - this.populate(); - } - - // If the id has changed we need to clear the album - // files and fetch from the server. - - if (prevProps.id !== id) { + if (!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) || + (prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) { this.unpopulate(); this.populate(); } @@ -201,8 +174,6 @@ class AlbumDetailsConnector extends Component { AlbumDetailsConnector.propTypes = { id: PropTypes.number, anyReleaseOk: PropTypes.bool, - isRenamingFiles: PropTypes.bool.isRequired, - isRenamingArtist: PropTypes.bool.isRequired, isAlbumFetching: PropTypes.bool, isAlbumPopulated: PropTypes.bool, foreignAlbumId: PropTypes.string.isRequired, diff --git a/frontend/src/Album/Details/AlbumDetailsMedium.js b/frontend/src/Album/Details/AlbumDetailsMedium.js index 9e80e2c7a..fb665cb88 100644 --- a/frontend/src/Album/Details/AlbumDetailsMedium.js +++ b/frontend/src/Album/Details/AlbumDetailsMedium.js @@ -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 { :
- {translate('NoTracksInThisMedium')} + No tracks in this medium
}
diff --git a/frontend/src/Album/Details/TrackRow.css b/frontend/src/Album/Details/TrackRow.css index 912c00101..3162ac9cc 100644 --- a/frontend/src/Album/Details/TrackRow.css +++ b/frontend/src/Album/Details/TrackRow.css @@ -23,7 +23,6 @@ } .duration, -.size, .status { composes: cell from '~Components/Table/Cells/TableRowCell.css'; @@ -35,9 +34,3 @@ width: 55px; } - -.indexerFlags { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 50px; -} diff --git a/frontend/src/Album/Details/TrackRow.css.d.ts b/frontend/src/Album/Details/TrackRow.css.d.ts index 79bbdaf43..7b473fe05 100644 --- a/frontend/src/Album/Details/TrackRow.css.d.ts +++ b/frontend/src/Album/Details/TrackRow.css.d.ts @@ -4,9 +4,7 @@ interface CssExports { 'audio': string; 'customFormatScore': string; 'duration': string; - 'indexerFlags': string; 'monitored': string; - 'size': string; 'status': string; 'title': string; 'trackNumber': string; diff --git a/frontend/src/Album/Details/TrackRow.js b/frontend/src/Album/Details/TrackRow.js index db128d493..a8deb3e98 100644 --- a/frontend/src/Album/Details/TrackRow.js +++ b/frontend/src/Album/Details/TrackRow.js @@ -2,19 +2,14 @@ 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'; @@ -33,10 +28,8 @@ class TrackRow extends Component { title, duration, trackFilePath, - trackFileSize, customFormats, customFormatScore, - indexerFlags, columns, deleteTrackFile } = this.props; @@ -146,41 +139,12 @@ class TrackRow extends Component { customFormats.length )} tooltip={} - position={tooltipPositions.LEFT} + position={tooltipPositions.BOTTOM} /> ); } - if (name === 'indexerFlags') { - return ( - - {indexerFlags ? ( - } - title={translate('IndexerFlags')} - body={} - position={tooltipPositions.LEFT} - /> - ) : null} - - ); - } - - if (name === 'size') { - return ( - - {!!trackFileSize && formatBytes(trackFileSize)} - - ); - } - if (name === 'status') { return ( { return { 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 }; } ); diff --git a/frontend/src/Album/Edit/EditAlbumModalContent.js b/frontend/src/Album/Edit/EditAlbumModalContent.js index dafc0312d..8c5ed58a6 100644 --- a/frontend/src/Album/Edit/EditAlbumModalContent.js +++ b/frontend/src/Album/Edit/EditAlbumModalContent.js @@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component { title, artistName, albumType, - statistics = {}, + statistics, item, isSaving, onInputChange, @@ -43,10 +43,6 @@ class EditAlbumModalContent extends Component { ...otherProps } = this.props; - const { - trackFileCount = 0 - } = statistics; - const { monitored, anyReleaseOk, @@ -100,7 +96,7 @@ class EditAlbumModalContent extends Component { type={inputTypes.ALBUM_RELEASE_SELECT} name="releases" helpText={translate('ReleasesHelpText')} - isDisabled={anyReleaseOk.value && trackFileCount > 0} + isDisabled={anyReleaseOk.value && statistics.trackFileCount > 0} albumReleases={releases} onChange={onInputChange} /> diff --git a/frontend/src/Album/IndexerFlags.tsx b/frontend/src/Album/IndexerFlags.tsx deleted file mode 100644 index 74e2e033c..000000000 --- a/frontend/src/Album/IndexerFlags.tsx +++ /dev/null @@ -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 ? ( -
    - {flags.map((flag, index) => { - return
  • {flag.name}
  • ; - })} -
- ) : null; -} - -export default IndexerFlags; diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModal.js b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js index 6ce488615..5049658a0 100644 --- a/frontend/src/Album/Search/AlbumInteractiveSearchModal.js +++ b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js @@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) { return ( diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js index 370f67ab1..97261ee35 100644 --- a/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js +++ b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js @@ -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 ( - {albumTitle === undefined ? - translate('InteractiveSearchModalHeader') : - translate('InteractiveSearchModalHeaderTitle', { title: albumTitle }) - } + Interactive Search {albumId != null && `- ${albumTitle}`} @@ -36,7 +32,7 @@ function AlbumInteractiveSearchModalContent(props) { diff --git a/frontend/src/Album/TrackQuality.js b/frontend/src/Album/TrackQuality.js index 6eac5d2f8..ca94227a0 100644 --- a/frontend/src/Album/TrackQuality.js +++ b/frontend/src/Album/TrackQuality.js @@ -3,7 +3,6 @@ import React from 'react'; import Label from 'Components/Label'; import { kinds } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; -import translate from 'Utilities/String/translate'; function getTooltip(title, quality, size) { if (!title) { @@ -27,44 +26,13 @@ function getTooltip(title, quality, size) { return title; } -function revisionLabel(className, quality, showRevision) { - if (!showRevision) { - return; - } - - if (quality.revision.isRepack) { - return ( - - ); - } - - if (quality.revision.version && quality.revision.version > 1) { - return ( - - ); - } -} - function TrackQuality(props) { const { className, title, quality, size, - isCutoffNotMet, - showRevision + isCutoffNotMet } = props; if (!quality) { @@ -72,15 +40,13 @@ function TrackQuality(props) { } return ( - - {revisionLabel(className, quality, showRevision)} - + ); } @@ -89,13 +55,11 @@ TrackQuality.propTypes = { title: PropTypes.string, quality: PropTypes.object.isRequired, size: PropTypes.number, - isCutoffNotMet: PropTypes.bool, - showRevision: PropTypes.bool + isCutoffNotMet: PropTypes.bool }; TrackQuality.defaultProps = { - title: '', - showRevision: false + title: '' }; export default TrackQuality; diff --git a/frontend/src/AlbumStudio/AlbumStudio.css b/frontend/src/AlbumStudio/AlbumStudio.css new file mode 100644 index 000000000..033279591 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudio.css @@ -0,0 +1,36 @@ +.pageContentBodyWrapper { + display: flex; + flex: 1 0 1px; + overflow: hidden; +} + +.contentBody { + composes: contentBody from '~Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; +} + +.tableInnerContentBody { + composes: innerContentBody from '~Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.contentBodyContainer { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +@media only screen and (max-width: $breakpointSmall) { + .pageContentBodyWrapper { + flex-basis: auto; + } + + .contentBody { + flex-basis: 1px; + } +} diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css.d.ts b/frontend/src/AlbumStudio/AlbumStudio.css.d.ts similarity index 55% rename from frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css.d.ts rename to frontend/src/AlbumStudio/AlbumStudio.css.d.ts index 4c59f6545..9937ea245 100644 --- a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css.d.ts +++ b/frontend/src/AlbumStudio/AlbumStudio.css.d.ts @@ -1,10 +1,10 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'labelIcon': string; - 'message': string; - 'modalFooter': string; - 'selected': string; + 'contentBody': string; + 'contentBodyContainer': string; + 'pageContentBodyWrapper': string; + 'tableInnerContentBody': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/AlbumStudio/AlbumStudio.js b/frontend/src/AlbumStudio/AlbumStudio.js new file mode 100644 index 000000000..fb4d1fa4d --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudio.js @@ -0,0 +1,443 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { CellMeasurer, CellMeasurerCache } from 'react-virtualized'; +import NoArtist from 'Artist/NoArtist'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageJumpBar from 'Components/Page/PageJumpBar'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import VirtualTable from 'Components/Table/VirtualTable'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; +import { align, sortDirections } from 'Helpers/Props'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import AlbumStudioFilterModalConnector from './AlbumStudioFilterModalConnector'; +import AlbumStudioFooter from './AlbumStudioFooter'; +import AlbumStudioRowConnector from './AlbumStudioRowConnector'; +import AlbumStudioTableHeader from './AlbumStudioTableHeader'; +import styles from './AlbumStudio.css'; + +const columns = [ + { + name: 'status', + isVisible: true + }, + { + name: 'sortName', + label: () => translate('Name'), + isSortable: true, + isVisible: true + }, + { + name: 'albumCount', + label: () => translate('Albums'), + isSortable: false, + isVisible: true + } +]; + +class AlbumStudio extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + estimatedRowSize: 100, + scroller: null, + jumpBarItems: { order: [] }, + scrollIndex: null, + jumpCount: 0, + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + + this.cache = new CellMeasurerCache({ + defaultHeight: 100, + fixedWidth: true + }); + } + + componentDidMount() { + this.setSelectedState(); + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = this.props; + + const { + scrollIndex, + jumpCount + } = this.state; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.onSelectAllChange({ value: false }); + } + + // nasty hack to fix react-virtualized jumping incorrectly + // due to variable row heights + if (scrollIndex != null && scrollIndex > 0) { + if (jumpCount === 0) { + this.setState({ + scrollIndex: scrollIndex - 1, + jumpCount: 1 + }); + } else if (jumpCount === 1) { + this.setState({ + scrollIndex: scrollIndex + 1, + jumpCount: 2 + }); + } else { + this.setState({ + scrollIndex: null, + jumpCount: 0 + }); + } + } + } + + // + // Control + + setScrollerRef = (ref) => { + this.setState({ scroller: ref }); + }; + + setJumpBarItems() { + const { + items, + sortKey, + sortDirection + } = this.props; + + // Reset if not sorting by sortName + if (sortKey !== 'sortName') { + this.setState({ jumpBarItems: { order: [] } }); + return; + } + + const characters = _.reduce(items, (acc, item) => { + let char = item.sortName.charAt(0); + + if (!isNaN(char)) { + char = '#'; + } + + if (char in acc) { + acc[char] = acc[char] + 1; + } else { + acc[char] = 1; + } + + return acc; + }, {}); + + const order = Object.keys(characters).sort(); + + // Reverse if sorting descending + if (sortDirection === sortDirections.DESCENDING) { + order.reverse(); + } + + const jumpBarItems = { + characters, + order + }; + + this.setState({ jumpBarItems }); + } + + getSelectedIds = () => { + if (this.state.allUnselected) { + return []; + } + return getSelectedIds(this.state.selectedState); + }; + + setSelectedState = () => { + const { + items + } = this.props; + + const { + selectedState + } = this.state; + + const newSelectedState = {}; + + items.forEach((artist) => { + const isItemSelected = selectedState[artist.id]; + + if (isItemSelected) { + newSelectedState[artist.id] = isItemSelected; + } else { + newSelectedState[artist.id] = false; + } + }); + + const selectedCount = getSelectedIds(newSelectedState).length; + const newStateCount = Object.keys(newSelectedState).length; + let isAllSelected = false; + let isAllUnselected = false; + + if (selectedCount === 0) { + isAllUnselected = true; + } else if (selectedCount === newStateCount) { + isAllSelected = true; + } + + this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected }); + }; + + estimateRowHeight = (width) => { + const { + albumCount, + items + } = this.props; + + if (albumCount === undefined || albumCount === 0 || items.length === 0) { + return 100; + } + + // guess 250px per album entry + // available width is total width less 186px for select, status etc + const cols = Math.max(Math.floor((width - 186) / 250), 1); + const albumsPerArtist = albumCount / items.length; + const albumRowsPerArtist = albumsPerArtist / cols; + + // each row is 23px per album row plus 16px padding + return albumRowsPerArtist * 23 + 16; + }; + + rowRenderer = ({ key, rowIndex, parent, style }) => { + const { + items + } = this.props; + + const { + selectedState + } = this.state; + + const item = items[rowIndex]; + + return ( + + {({ registerChild }) => ( + + + + )} + + ); + }; + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + }; + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + }; + + onSelectAllPress = () => { + this.onSelectAllChange({ value: !this.state.allSelected }); + }; + + onUpdateSelectedPress = (changes) => { + this.props.onUpdateSelectedPress({ + artistIds: this.getSelectedIds(), + ...changes + }); + }; + + onJumpBarItemPress = (jumpToCharacter) => { + const scrollIndex = getIndexOfFirstCharacter(this.props.items, jumpToCharacter); + + if (scrollIndex != null) { + this.setState({ scrollIndex }); + } + }; + + onGridRecompute = (width) => { + this.setJumpBarItems(); + this.setSelectedState(); + this.setState({ estimatedRowSize: this.estimateRowHeight(width) }); + this.cache.clearAll(); + }; + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + totalItems, + items, + selectedFilterKey, + filters, + customFilters, + sortKey, + sortDirection, + isSaving, + saveError, + isSmallScreen, + onSortPress, + onFilterSelect + } = this.props; + + const { + allSelected, + allUnselected, + estimatedRowSize, + scroller, + jumpBarItems, + scrollIndex + } = this.state; + + return ( + + + + + + + + +
+ + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
{getErrorMessage(error, 'Failed to load artist from API')}
+ } + + { + !error && isPopulated && !!items.length && +
+ + } + sortKey={sortKey} + sortDirection={sortDirection} + deferredMeasurementCache={this.cache} + rowHeight={this.cache.rowHeight} + estimatedRowSize={estimatedRowSize} + onRecompute={this.onGridRecompute} + /> +
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + { + isPopulated && !!jumpBarItems.order.length && + + } +
+ + +
+ ); + } +} + +AlbumStudio.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + totalItems: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + albumCount: PropTypes.number.isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + isSmallScreen: PropTypes.bool.isRequired, + onSortPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onUpdateSelectedPress: PropTypes.func.isRequired +}; + +export default AlbumStudio; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css b/frontend/src/AlbumStudio/AlbumStudioAlbum.css similarity index 100% rename from frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css rename to frontend/src/AlbumStudio/AlbumStudioAlbum.css diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css.d.ts b/frontend/src/AlbumStudio/AlbumStudioAlbum.css.d.ts similarity index 100% rename from frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css.d.ts rename to frontend/src/AlbumStudio/AlbumStudioAlbum.css.d.ts diff --git a/frontend/src/AlbumStudio/AlbumStudioAlbum.js b/frontend/src/AlbumStudio/AlbumStudioAlbum.js new file mode 100644 index 000000000..5dc9dc233 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioAlbum.js @@ -0,0 +1,102 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import translate from 'Utilities/String/translate'; +import styles from './AlbumStudioAlbum.css'; + +class AlbumStudioAlbum extends Component { + + // + // Listeners + + onAlbumMonitoredPress = () => { + const { + id, + monitored + } = this.props; + + this.props.onAlbumMonitoredPress(id, !monitored); + }; + + // + // Render + + render() { + const { + title, + disambiguation, + albumType, + monitored, + statistics, + isSaving + } = this.props; + + const { + trackFileCount, + totalTrackCount, + percentOfTracks + } = statistics; + + return ( +
+
+ + + + { + disambiguation ? `${title} (${disambiguation})` : `${title}` + } + +
+ +
+ + { + `${albumType}` + } + +
+ +
+ { + totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}` + } +
+
+ ); + } +} + +AlbumStudioAlbum.propTypes = { + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + disambiguation: PropTypes.string, + albumType: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + statistics: PropTypes.object.isRequired, + isSaving: PropTypes.bool.isRequired, + onAlbumMonitoredPress: PropTypes.func.isRequired +}; + +AlbumStudioAlbum.defaultProps = { + isSaving: false, + statistics: { + trackFileCount: 0, + totalTrackCount: 0, + percentOfTracks: 0 + } +}; + +export default AlbumStudioAlbum; diff --git a/frontend/src/AlbumStudio/AlbumStudioConnector.js b/frontend/src/AlbumStudio/AlbumStudioConnector.js new file mode 100644 index 000000000..25fedafa4 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioConnector.js @@ -0,0 +1,116 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions'; +import { saveAlbumStudio, setAlbumStudioFilter, setAlbumStudioSort } from 'Store/Actions/albumStudioActions'; +import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AlbumStudio from './AlbumStudio'; + +function createAlbumFetchStateSelector() { + return createSelector( + (state) => state.albums.items.length, + (state) => state.albums.isFetching, + (state) => state.albums.isPopulated, + (length, isFetching, isPopulated) => { + const albumCount = (!isFetching && isPopulated) ? length : 0; + return { + albumCount, + isFetching, + isPopulated + }; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createAlbumFetchStateSelector(), + createArtistClientSideCollectionItemsSelector('albumStudio'), + createDimensionsSelector(), + (albums, artist, dimensionsState) => { + const isPopulated = albums.isPopulated && artist.isPopulated; + const isFetching = artist.isFetching || albums.isFetching; + return { + ...artist, + isPopulated, + isFetching, + albumCount: albums.albumCount, + isSmallScreen: dimensionsState.isSmallScreen + }; + } + ); +} + +const mapDispatchToProps = { + fetchAlbums, + clearAlbums, + setAlbumStudioSort, + setAlbumStudioFilter, + saveAlbumStudio +}; + +class AlbumStudioConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.populate(); + } + + componentWillUnmount() { + this.unpopulate(); + } + + // + // Control + + populate = () => { + this.props.fetchAlbums(); + }; + + unpopulate = () => { + this.props.clearAlbums(); + }; + + // + // Listeners + + onSortPress = (sortKey) => { + this.props.setAlbumStudioSort({ sortKey }); + }; + + onFilterSelect = (selectedFilterKey) => { + this.props.setAlbumStudioFilter({ selectedFilterKey }); + }; + + onUpdateSelectedPress = (payload) => { + this.props.saveAlbumStudio(payload); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +AlbumStudioConnector.propTypes = { + setAlbumStudioSort: PropTypes.func.isRequired, + setAlbumStudioFilter: PropTypes.func.isRequired, + fetchAlbums: PropTypes.func.isRequired, + clearAlbums: PropTypes.func.isRequired, + saveAlbumStudio: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioConnector); diff --git a/frontend/src/AlbumStudio/AlbumStudioFilterModalConnector.js b/frontend/src/AlbumStudio/AlbumStudioFilterModalConnector.js new file mode 100644 index 000000000..db378a7f2 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioFilterModalConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setAlbumStudioFilter } from 'Store/Actions/albumStudioActions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artist.items, + (state) => state.albumStudio.filterBuilderProps, + (sectionItems, filterBuilderProps) => { + return { + sectionItems, + filterBuilderProps, + customFilterType: 'albumStudio' + }; + } + ); +} + +const mapDispatchToProps = { + dispatchSetFilter: setAlbumStudioFilter +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal); diff --git a/frontend/src/AlbumStudio/AlbumStudioFooter.css b/frontend/src/AlbumStudio/AlbumStudioFooter.css new file mode 100644 index 000000000..11ea5496a --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioFooter.css @@ -0,0 +1,14 @@ +.inputContainer { + margin-right: 20px; +} + +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.updateSelectedButton { + composes: button from '~Components/Link/SpinnerButton.css'; + + height: 35px; +} diff --git a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css.d.ts b/frontend/src/AlbumStudio/AlbumStudioFooter.css.d.ts similarity index 67% rename from frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css.d.ts rename to frontend/src/AlbumStudio/AlbumStudioFooter.css.d.ts index 65c237dff..83e60938b 100644 --- a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css.d.ts +++ b/frontend/src/AlbumStudio/AlbumStudioFooter.css.d.ts @@ -1,7 +1,9 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'message': string; + 'inputContainer': string; + 'label': string; + 'updateSelectedButton': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/AlbumStudio/AlbumStudioFooter.js b/frontend/src/AlbumStudio/AlbumStudioFooter.js new file mode 100644 index 000000000..8543a0347 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioFooter.js @@ -0,0 +1,174 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import MonitorAlbumsSelectInput from 'Components/Form/MonitorAlbumsSelectInput'; +import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import { inputTypes, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './AlbumStudioFooter.css'; + +const NO_CHANGE = 'noChange'; + +class AlbumStudioFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + monitored: NO_CHANGE, + monitor: NO_CHANGE, + monitorNewItems: NO_CHANGE + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = this.props; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.setState({ + monitored: NO_CHANGE, + monitor: NO_CHANGE, + monitorNewItems: NO_CHANGE + }); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + }; + + onUpdateSelectedPress = () => { + const { + monitor, + monitored, + monitorNewItems + } = this.state; + + const changes = {}; + + if (monitored !== NO_CHANGE) { + changes.monitored = monitored === 'monitored'; + } + + if (monitor !== NO_CHANGE) { + changes.monitor = monitor; + } + + if (monitorNewItems !== NO_CHANGE) { + changes.monitorNewItems = monitorNewItems; + } + + this.props.onUpdateSelectedPress(changes); + }; + + // + // Render + + render() { + const { + selectedCount, + isSaving + } = this.props; + + const { + monitored, + monitor, + monitorNewItems + } = this.state; + + const monitoredOptions = [ + { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, + { key: 'monitored', value: translate('Monitored') }, + { key: 'unmonitored', value: translate('Unmonitored') } + ]; + + const noChanges = monitored === NO_CHANGE && + monitor === NO_CHANGE && + monitorNewItems === NO_CHANGE; + + return ( + +
+
+ {translate('MonitorArtist')} +
+ + +
+ +
+
+ {translate('MonitorExistingAlbums')} +
+ + +
+ +
+
+ {translate('MonitorNewAlbums')} +
+ + +
+ +
+
+ {translate('CountArtistsSelected', { selectedCount })} +
+ + + {translate('UpdateSelected')} + +
+
+ ); + } +} + +AlbumStudioFooter.propTypes = { + selectedCount: PropTypes.number.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + onUpdateSelectedPress: PropTypes.func.isRequired +}; + +export default AlbumStudioFooter; diff --git a/frontend/src/AlbumStudio/AlbumStudioRow.css b/frontend/src/AlbumStudio/AlbumStudioRow.css new file mode 100644 index 000000000..d8def1d50 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioRow.css @@ -0,0 +1,41 @@ +.cell { + composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; + + display: flex; + align-items: center; +} + +.selectCell { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + display: flex; + align-items: center; +} + +.status { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + display: flex; + align-items: center; + padding: 0; + min-width: 60px; +} + +.title { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + display: flex; + align-items: center; + + flex-shrink: 0; + min-width: 110px; +} + +.albums { + composes: cell; + + display: flex; + flex-grow: 4; + flex-wrap: wrap; + min-width: 400px; +} diff --git a/frontend/src/Parse/ParseResultItem.css.d.ts b/frontend/src/AlbumStudio/AlbumStudioRow.css.d.ts similarity index 69% rename from frontend/src/Parse/ParseResultItem.css.d.ts rename to frontend/src/AlbumStudio/AlbumStudioRow.css.d.ts index bcf268e50..9f9ac8eac 100644 --- a/frontend/src/Parse/ParseResultItem.css.d.ts +++ b/frontend/src/AlbumStudio/AlbumStudioRow.css.d.ts @@ -1,7 +1,10 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'item': string; + 'albums': string; + 'cell': string; + 'selectCell': string; + 'status': string; 'title': string; } export const cssExports: CssExports; diff --git a/frontend/src/AlbumStudio/AlbumStudioRow.js b/frontend/src/AlbumStudio/AlbumStudioRow.js new file mode 100644 index 000000000..5a13f442d --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioRow.js @@ -0,0 +1,95 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; +import AlbumStudioAlbum from './AlbumStudioAlbum'; +import styles from './AlbumStudioRow.css'; + +class AlbumStudioRow extends Component { + + // + // Render + + render() { + const { + artistId, + status, + foreignArtistId, + artistName, + artistType, + monitored, + albums, + isSaving, + isSelected, + onSelectedChange, + onArtistMonitoredPress, + onAlbumMonitoredPress + } = this.props; + + return ( + <> + + + + + + + + + + { + albums.map((album) => { + return ( + + ); + }) + } + + + ); + } +} + +AlbumStudioRow.propTypes = { + artistId: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + foreignArtistId: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + artistType: PropTypes.string, + monitored: PropTypes.bool.isRequired, + albums: PropTypes.arrayOf(PropTypes.object).isRequired, + isSaving: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired, + onArtistMonitoredPress: PropTypes.func.isRequired, + onAlbumMonitoredPress: PropTypes.func.isRequired +}; + +AlbumStudioRow.defaultProps = { + isSaving: false +}; + +export default AlbumStudioRow; diff --git a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js new file mode 100644 index 000000000..fd0bd21dc --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js @@ -0,0 +1,94 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { toggleAlbumsMonitored } from 'Store/Actions/albumActions'; +import { toggleArtistMonitored } from 'Store/Actions/artistActions'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import AlbumStudioRow from './AlbumStudioRow'; + +// Use a const to share the reselect cache between instances +const getAlbumMap = createSelector( + (state) => state.albums.items, + (albums) => { + return albums.reduce((acc, curr) => { + (acc[curr.artistId] = acc[curr.artistId] || []).push(curr); + return acc; + }, {}); + } +); + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + getAlbumMap, + (artist, albumMap) => { + const albumsInArtist = albumMap.hasOwnProperty(artist.id) ? albumMap[artist.id] : []; + const sortedAlbums = _.orderBy(albumsInArtist, 'releaseDate', 'desc'); + + return { + ...artist, + artistId: artist.id, + artistName: artist.artistName, + monitored: artist.monitored, + status: artist.status, + isSaving: artist.isSaving, + albums: sortedAlbums + }; + } + ); +} + +const mapDispatchToProps = { + toggleArtistMonitored, + toggleAlbumsMonitored +}; + +class AlbumStudioRowConnector extends Component { + + // + // Listeners + + onArtistMonitoredPress = () => { + const { + artistId, + monitored + } = this.props; + + this.props.toggleArtistMonitored({ + artistId, + monitored: !monitored + }); + }; + + onAlbumMonitoredPress = (albumId, monitored) => { + const albumIds = [albumId]; + this.props.toggleAlbumsMonitored({ + albumIds, + monitored + }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +AlbumStudioRowConnector.propTypes = { + artistId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + toggleArtistMonitored: PropTypes.func.isRequired, + toggleAlbumsMonitored: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector); diff --git a/frontend/src/AlbumStudio/AlbumStudioTableHeader.css b/frontend/src/AlbumStudio/AlbumStudioTableHeader.css new file mode 100644 index 000000000..da21f0553 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioTableHeader.css @@ -0,0 +1,18 @@ +.status { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 60px; + padding: 0; +} + +.sortName { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 110px; +} + +.albumCount { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + padding: 12px; +} diff --git a/frontend/src/Artist/Index/Table/AlbumsCell.css.d.ts b/frontend/src/AlbumStudio/AlbumStudioTableHeader.css.d.ts similarity index 81% rename from frontend/src/Artist/Index/Table/AlbumsCell.css.d.ts rename to frontend/src/AlbumStudio/AlbumStudioTableHeader.css.d.ts index 93d667287..3978a6c39 100644 --- a/frontend/src/Artist/Index/Table/AlbumsCell.css.d.ts +++ b/frontend/src/AlbumStudio/AlbumStudioTableHeader.css.d.ts @@ -2,6 +2,8 @@ // Please do not change this file! interface CssExports { 'albumCount': string; + 'sortName': string; + 'status': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/AlbumStudio/AlbumStudioTableHeader.js b/frontend/src/AlbumStudio/AlbumStudioTableHeader.js new file mode 100644 index 000000000..8d18babbe --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioTableHeader.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; +import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; +import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell'; +import styles from './AlbumStudioTableHeader.css'; + +function AlbumStudioTableHeader(props) { + const { + columns, + allSelected, + allUnselected, + onSelectAllChange, + ...otherProps + } = props; + + return ( + + + { + columns.map((column) => { + const { + name, + label, + isSortable, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + return ( + + {typeof label === 'function' ? label() : label} + + ); + }) + } + + ); +} + +AlbumStudioTableHeader.propTypes = { + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + onSelectAllChange: PropTypes.func.isRequired +}; + +export default AlbumStudioTableHeader; diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js index 9e8d508ac..3871b14e9 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.js @@ -12,10 +12,11 @@ function App({ store, history }) { - - - - + + + + + diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index c1004d36d..223e0f90e 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -5,13 +5,15 @@ import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector'; import HistoryConnector from 'Activity/History/HistoryConnector'; import QueueConnector from 'Activity/Queue/QueueConnector'; import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector'; +import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector'; import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector'; -import ArtistIndex from 'Artist/Index/ArtistIndex'; +import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector'; +import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector'; 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 +31,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'; @@ -49,7 +51,7 @@ function AppRoutes(props) { { @@ -76,28 +78,12 @@ function AppRoutes(props) { { - return ( - - ); - }} + component={ArtistEditorConnector} /> { - return ( - - ); - }} + component={AlbumStudioConnector} /> - {translate('AppUpdated')} + {translate('AppUpdated', { appName: 'Lidarr' })}
- +
{ diff --git a/frontend/src/App/ApplyTheme.js b/frontend/src/App/ApplyTheme.js new file mode 100644 index 000000000..7e937e586 --- /dev/null +++ b/frontend/src/App/ApplyTheme.js @@ -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 {children}; +} + +ApplyTheme.propTypes = { + theme: PropTypes.string.isRequired, + children: PropTypes.object.isRequired +}; + +export default connect(createMapStateToProps)(ApplyTheme); diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx deleted file mode 100644 index e04dda8c4..000000000 --- a/frontend/src/App/ApplyTheme.tsx +++ /dev/null @@ -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 {children}; -} - -export default ApplyTheme; diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.js index 5c08f491f..1d1af0152 100644 --- a/frontend/src/App/ConnectionLostModal.js +++ b/frontend/src/App/ConnectionLostModal.js @@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
- {translate('ConnectionLostToBackend')} + {translate('ConnectionLostToBackend', { appName: 'Lidarr' })}
- {translate('ConnectionLostReconnect')} + {translate('ConnectionLostReconnect', { appName: 'Lidarr' })}
diff --git a/frontend/src/App/SelectContext.tsx b/frontend/src/App/SelectContext.tsx deleted file mode 100644 index 66be388ce..000000000 --- a/frontend/src/App/SelectContext.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { cloneDeep } from 'lodash'; -import React, { useCallback, useEffect } from 'react'; -import useSelectState, { SelectState } from 'Helpers/Hooks/useSelectState'; -import ModelBase from './ModelBase'; - -export type SelectContextAction = - | { type: 'reset' } - | { type: 'selectAll' } - | { type: 'unselectAll' } - | { - type: 'toggleSelected'; - id: number; - isSelected: boolean; - shiftKey: boolean; - } - | { - type: 'removeItem'; - id: number; - } - | { - type: 'updateItems'; - items: ModelBase[]; - }; - -export type SelectDispatch = (action: SelectContextAction) => void; - -interface SelectProviderOptions { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - children: any; - items: Array; -} - -const SelectContext = React.createContext< - [SelectState, SelectDispatch] | undefined ->(cloneDeep(undefined)); - -export function SelectProvider( - props: SelectProviderOptions -) { - const { items } = props; - const [state, dispatch] = useSelectState(); - - const dispatchWrapper = useCallback( - (action: SelectContextAction) => { - switch (action.type) { - case 'reset': - case 'removeItem': - dispatch(action); - break; - - default: - dispatch({ - ...action, - items, - }); - break; - } - }, - [items, dispatch] - ); - - const value: [SelectState, SelectDispatch] = [state, dispatchWrapper]; - - useEffect(() => { - dispatch({ type: 'updateItems', items }); - }, [items, dispatch]); - - return ( - - {props.children} - - ); -} - -export function useSelect() { - const context = React.useContext(SelectContext); - - if (context === undefined) { - throw new Error('useSelect must be used within a SelectProvider'); - } - - return context; -} diff --git a/frontend/src/App/State/AlbumAppState.ts b/frontend/src/App/State/AlbumAppState.ts deleted file mode 100644 index e03d4a497..000000000 --- a/frontend/src/App/State/AlbumAppState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Album from 'Album/Album'; -import AppSectionState, { - AppSectionDeleteState, -} from 'App/State/AppSectionState'; - -interface AlbumAppState extends AppSectionState, AppSectionDeleteState {} - -export default AlbumAppState; diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index cabc39b1c..d511963fc 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,5 +1,4 @@ import SortDirection from 'Helpers/Props/SortDirection'; -import { FilterBuilderProp } from './AppState'; export interface Error { responseJSON: { @@ -21,10 +20,6 @@ export interface PagedAppSectionState { pageSize: number; } -export interface AppSectionFilterState { - filterBuilderProps: FilterBuilderProp[]; -} - export interface AppSectionSchemaState { isSchemaFetching: boolean; isSchemaPopulated: boolean; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index cb8da5987..8c8b99fba 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -1,15 +1,5 @@ -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'; interface FilterBuilderPropOption { id: string; @@ -43,30 +33,9 @@ export interface CustomFilter { filers: PropertyFilter[]; } -export interface AppSectionState { - version: string; - dimensions: { - isSmallScreen: boolean; - width: number; - height: number; - }; -} - interface AppState { - albums: AlbumAppState; - app: AppSectionState; - artist: ArtistAppState; - artistIndex: ArtistIndexAppState; - calendar: CalendarAppState; - commands: CommandAppState; - history: HistoryAppState; - parse: ParseAppState; - queue: QueueAppState; settings: SettingsAppState; tags: TagsAppState; - trackFiles: TrackFilesAppState; - tracksSelection: TracksAppState; - system: SystemAppState; } export default AppState; diff --git a/frontend/src/App/State/ArtistAppState.ts b/frontend/src/App/State/ArtistAppState.ts deleted file mode 100644 index 9e0628df7..000000000 --- a/frontend/src/App/State/ArtistAppState.ts +++ /dev/null @@ -1,72 +0,0 @@ -import AppSectionState, { - AppSectionDeleteState, - AppSectionSaveState, -} from 'App/State/AppSectionState'; -import Artist from 'Artist/Artist'; -import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; -import { Filter, FilterBuilderProp } from './AppState'; - -export interface ArtistIndexAppState { - sortKey: string; - sortDirection: SortDirection; - secondarySortKey: string; - secondarySortDirection: SortDirection; - view: string; - - posterOptions: { - detailedProgressBar: boolean; - size: string; - showTitle: boolean; - showMonitored: boolean; - showQualityProfile: boolean; - showNextAlbum: boolean; - showSearchAction: boolean; - }; - - bannerOptions: { - detailedProgressBar: boolean; - size: string; - showTitle: boolean; - showMonitored: boolean; - showQualityProfile: boolean; - showNextAlbum: boolean; - showSearchAction: boolean; - }; - - overviewOptions: { - detailedProgressBar: boolean; - size: string; - showMonitored: boolean; - showQualityProfile: boolean; - showLastAlbum: boolean; - showAdded: boolean; - showAlbumCount: boolean; - showPath: boolean; - showSizeOnDisk: boolean; - showSearchAction: boolean; - }; - - tableOptions: { - showBanners: boolean; - showSearchAction: boolean; - }; - - selectedFilterKey: string; - filterBuilderProps: FilterBuilderProp[]; - filters: Filter[]; - columns: Column[]; -} - -interface ArtistAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState { - itemMap: Record; - - deleteOptions: { - addImportListExclusion: boolean; - }; -} - -export default ArtistAppState; diff --git a/frontend/src/App/State/CalendarAppState.ts b/frontend/src/App/State/CalendarAppState.ts deleted file mode 100644 index 503d2c25b..000000000 --- a/frontend/src/App/State/CalendarAppState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Album from 'Album/Album'; -import AppSectionState, { - AppSectionFilterState, -} from 'App/State/AppSectionState'; - -interface CalendarAppState - extends AppSectionState, - AppSectionFilterState {} - -export default CalendarAppState; diff --git a/frontend/src/App/State/ClientSideCollectionAppState.ts b/frontend/src/App/State/ClientSideCollectionAppState.ts deleted file mode 100644 index f4110ef73..000000000 --- a/frontend/src/App/State/ClientSideCollectionAppState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CustomFilter } from './AppState'; - -interface ClientSideCollectionAppState { - totalItems: number; - customFilters: CustomFilter[]; -} - -export default ClientSideCollectionAppState; diff --git a/frontend/src/App/State/CommandAppState.ts b/frontend/src/App/State/CommandAppState.ts deleted file mode 100644 index 1bde37371..000000000 --- a/frontend/src/App/State/CommandAppState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import AppSectionState from 'App/State/AppSectionState'; -import Command from 'Commands/Command'; - -export type CommandAppState = AppSectionState; - -export default CommandAppState; diff --git a/frontend/src/App/State/CustomFiltersAppState.ts b/frontend/src/App/State/CustomFiltersAppState.ts deleted file mode 100644 index 6ac4820c7..000000000 --- a/frontend/src/App/State/CustomFiltersAppState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import AppSectionState, { - AppSectionDeleteState, -} from 'App/State/AppSectionState'; -import { CustomFilter } from './AppState'; - -interface CustomFiltersAppState - extends AppSectionState, - AppSectionDeleteState {} - -export default CustomFiltersAppState; diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts deleted file mode 100644 index e368ff86e..000000000 --- a/frontend/src/App/State/HistoryAppState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import AppSectionState, { - AppSectionFilterState, -} from 'App/State/AppSectionState'; -import History from 'typings/History'; - -interface HistoryAppState - extends AppSectionState, - AppSectionFilterState {} - -export default HistoryAppState; diff --git a/frontend/src/App/State/ParseAppState.ts b/frontend/src/App/State/ParseAppState.ts deleted file mode 100644 index 827d5b1a7..000000000 --- a/frontend/src/App/State/ParseAppState.ts +++ /dev/null @@ -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; - -export default ParseAppState; diff --git a/frontend/src/App/State/QueueAppState.ts b/frontend/src/App/State/QueueAppState.ts deleted file mode 100644 index 05d74acac..000000000 --- a/frontend/src/App/State/QueueAppState.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Queue from 'typings/Queue'; -import AppSectionState, { - AppSectionFilterState, - AppSectionItemState, - Error, -} from './AppSectionState'; - -export interface QueueDetailsAppState extends AppSectionState { - params: unknown; -} - -export interface QueuePagedAppState - extends AppSectionState, - AppSectionFilterState { - isGrabbing: boolean; - grabError: Error; - isRemoving: boolean; - removeError: Error; -} - -interface QueueAppState { - status: AppSectionItemState; - details: QueueDetailsAppState; - paged: QueuePagedAppState; -} - -export default QueueAppState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index b387e13fd..4c0680956 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -1,28 +1,18 @@ 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, AppSectionDeleteState, AppSectionSaveState {} -export type GeneralAppState = AppSectionItemState; - export interface ImportListAppState extends AppSectionState, AppSectionDeleteState, @@ -37,40 +27,14 @@ export interface NotificationAppState extends AppSectionState, AppSectionDeleteState {} -export interface QualityProfilesAppState - extends AppSectionState, - AppSectionSchemaState {} - -export interface MetadataProfilesAppState - extends AppSectionState, - AppSectionSchemaState {} - -export interface CustomFormatAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - -export interface RootFolderAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - -export type IndexerFlagSettingsAppState = AppSectionState; -export type UiSettingsAppState = AppSectionItemState; +export type UiSettingsAppState = AppSectionState; 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; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts deleted file mode 100644 index 3c150fcfb..000000000 --- a/frontend/src/App/State/SystemAppState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import SystemStatus from 'typings/SystemStatus'; -import Update from 'typings/Update'; -import AppSectionState, { AppSectionItemState } from './AppSectionState'; - -export type SystemStatusAppState = AppSectionItemState; -export type UpdateAppState = AppSectionState; - -interface SystemAppState { - updates: UpdateAppState; - status: SystemStatusAppState; -} - -export default SystemAppState; diff --git a/frontend/src/App/State/TagsAppState.ts b/frontend/src/App/State/TagsAppState.ts index edaf3a158..d1f1d5a2f 100644 --- a/frontend/src/App/State/TagsAppState.ts +++ b/frontend/src/App/State/TagsAppState.ts @@ -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, - AppSectionDeleteState, - AppSectionSaveState {} - -interface TagsAppState extends AppSectionState, AppSectionDeleteState { - details: TagDetailAppState; -} +interface TagsAppState extends AppSectionState, AppSectionDeleteState {} export default TagsAppState; diff --git a/frontend/src/App/State/TrackFilesAppState.ts b/frontend/src/App/State/TrackFilesAppState.ts deleted file mode 100644 index 403ba904d..000000000 --- a/frontend/src/App/State/TrackFilesAppState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import AppSectionState, { - AppSectionDeleteState, -} from 'App/State/AppSectionState'; -import { TrackFile } from 'TrackFile/TrackFile'; - -interface TrackFilesAppState - extends AppSectionState, - AppSectionDeleteState {} - -export default TrackFilesAppState; diff --git a/frontend/src/App/State/TracksAppState.ts b/frontend/src/App/State/TracksAppState.ts deleted file mode 100644 index 22aaabed9..000000000 --- a/frontend/src/App/State/TracksAppState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import AppSectionState from 'App/State/AppSectionState'; -import Track from 'Track/Track'; - -type TracksAppState = AppSectionState; - -export default TracksAppState; diff --git a/frontend/src/Artist/Artist.ts b/frontend/src/Artist/Artist.ts deleted file mode 100644 index 813dbea08..000000000 --- a/frontend/src/Artist/Artist.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Album from 'Album/Album'; -import ModelBase from 'App/ModelBase'; - -export interface Image { - coverType: string; - url: string; - remoteUrl: string; -} - -export interface Statistics { - albumCount: number; - trackCount: number; - trackFileCount: number; - percentOfTracks: number; - sizeOnDisk: number; - totalTrackCount: number; -} - -export interface Ratings { - votes: number; - value: number; -} - -interface Artist extends ModelBase { - added: string; - foreignArtistId: string; - cleanName: string; - ended: boolean; - genres: string[]; - images: Image[]; - monitored: boolean; - overview: string; - path: string; - lastAlbum?: Album; - nextAlbum?: Album; - qualityProfileId: number; - metadataProfileId: number; - monitorNewItems: string; - ratings: Ratings; - rootFolderPath: string; - sortName: string; - statistics: Statistics; - status: string; - tags: number[]; - artistName: string; - artistType?: string; - disambiguation?: string; - isSaving?: boolean; -} - -export default Artist; diff --git a/frontend/src/Artist/ArtistBanner.js b/frontend/src/Artist/ArtistBanner.js index 5483912e1..b409667b1 100644 --- a/frontend/src/Artist/ArtistBanner.js +++ b/frontend/src/Artist/ArtistBanner.js @@ -15,10 +15,6 @@ function ArtistBanner(props) { } ArtistBanner.propTypes = { - ...ArtistImage.propTypes, - coverType: PropTypes.string, - placeholder: PropTypes.string, - overflow: PropTypes.bool, size: PropTypes.number.isRequired }; diff --git a/frontend/src/Artist/ArtistImage.js b/frontend/src/Artist/ArtistImage.js index 669cba8d8..6ebb48fe3 100644 --- a/frontend/src/Artist/ArtistImage.js +++ b/frontend/src/Artist/ArtistImage.js @@ -7,10 +7,13 @@ function findImage(images, coverType) { } function getUrl(image, coverType, size) { - const imageUrl = image?.url; + if (image) { + // Remove protocol + let url = image.url.replace(/^https?:/, ''); - if (imageUrl) { - return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`); + url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`); + + return url; } } diff --git a/frontend/src/Artist/ArtistLogo.js b/frontend/src/Artist/ArtistLogo.js index 93b91c2da..10358625f 100644 --- a/frontend/src/Artist/ArtistLogo.js +++ b/frontend/src/Artist/ArtistLogo.js @@ -10,10 +10,12 @@ function findLogo(images) { } function getLogoUrl(logo, size) { - const logoUrl = logo?.url; + if (logo) { + // Remove protocol + let url = logo.url.replace(/^https?:/, ''); + url = url.replace('logo.jpg', `logo-${size}.jpg`); - if (logoUrl) { - return logoUrl.replace('logo.jpg', `logo-${size}.jpg`); + return url; } } diff --git a/frontend/src/Artist/ArtistPoster.js b/frontend/src/Artist/ArtistPoster.js index de594e5b9..4eebd9ca4 100644 --- a/frontend/src/Artist/ArtistPoster.js +++ b/frontend/src/Artist/ArtistPoster.js @@ -15,10 +15,6 @@ function ArtistPoster(props) { } ArtistPoster.propTypes = { - ...ArtistImage.propTypes, - coverType: PropTypes.string, - placeholder: PropTypes.string, - overflow: PropTypes.bool, size: PropTypes.number.isRequired }; diff --git a/frontend/src/Artist/Delete/DeleteArtistModal.js b/frontend/src/Artist/Delete/DeleteArtistModal.js index c647b7735..8e0b87296 100644 --- a/frontend/src/Artist/Delete/DeleteArtistModal.js +++ b/frontend/src/Artist/Delete/DeleteArtistModal.js @@ -26,7 +26,6 @@ function DeleteArtistModal(props) { } DeleteArtistModal.propTypes = { - ...DeleteArtistModalContentConnector.propTypes, isOpen: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Delete/DeleteArtistModalContent.js index ac1e2b041..368c08107 100644 --- a/frontend/src/Artist/Delete/DeleteArtistModalContent.js +++ b/frontend/src/Artist/Delete/DeleteArtistModalContent.js @@ -23,7 +23,8 @@ class DeleteArtistModalContent extends Component { super(props, context); this.state = { - deleteFiles: false + deleteFiles: false, + addImportListExclusion: false }; } @@ -34,11 +35,16 @@ class DeleteArtistModalContent extends Component { this.setState({ deleteFiles: value }); }; + onAddImportListExclusionChange = ({ value }) => { + this.setState({ addImportListExclusion: value }); + }; + onDeleteArtistConfirmed = () => { const deleteFiles = this.state.deleteFiles; - const addImportListExclusion = this.props.deleteOptions.addImportListExclusion; + const addImportListExclusion = this.state.addImportListExclusion; this.setState({ deleteFiles: false }); + this.setState({ addImportListExclusion: false }); this.props.onDeletePress(deleteFiles, addImportListExclusion); }; @@ -50,18 +56,16 @@ class DeleteArtistModalContent extends Component { artistName, path, statistics, - deleteOptions, - onModalClose, - onDeleteOptionChange + onModalClose } = this.props; const { - trackFileCount = 0, - sizeOnDisk = 0 + trackFileCount, + sizeOnDisk } = statistics; const deleteFiles = this.state.deleteFiles; - const addImportListExclusion = deleteOptions.addImportListExclusion; + const addImportListExclusion = this.state.addImportListExclusion; let deleteFilesLabel = `Delete ${trackFileCount} Track Files`; let deleteFilesHelpText = translate('DeleteFilesHelpText'); @@ -113,7 +117,7 @@ class DeleteArtistModalContent extends Component { value={addImportListExclusion} helpText={translate('AddImportListExclusionArtistHelpText')} kind={kinds.DANGER} - onChange={onDeleteOptionChange} + onChange={this.onAddImportListExclusionChange} /> @@ -135,14 +139,14 @@ class DeleteArtistModalContent extends Component { @@ -154,14 +158,14 @@ DeleteArtistModalContent.propTypes = { artistName: PropTypes.string.isRequired, path: PropTypes.string.isRequired, statistics: PropTypes.object.isRequired, - deleteOptions: PropTypes.object.isRequired, - onDeleteOptionChange: PropTypes.func.isRequired, onDeletePress: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; DeleteArtistModalContent.defaultProps = { - statistics: {} + statistics: { + trackFileCount: 0 + } }; export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js index 321dc63a6..5b7fef377 100644 --- a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js +++ b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js @@ -1,44 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { deleteArtist, setDeleteOption } from 'Store/Actions/artistActions'; +import { deleteArtist } from 'Store/Actions/artistActions'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; import DeleteArtistModalContent from './DeleteArtistModalContent'; function createMapStateToProps() { return createSelector( - (state) => state.artist.deleteOptions, createArtistSelector(), - (deleteOptions, artist) => { - return { - ...artist, - deleteOptions - }; + (artist) => { + return artist; } ); } -function createMapDispatchToProps(dispatch, props) { - return { - onDeleteOptionChange(option) { - dispatch( - setDeleteOption({ - [option.name]: option.value - }) - ); - }, +const mapDispatchToProps = { + deleteArtist +}; - onDeletePress(deleteFiles, addImportListExclusion) { - dispatch( - deleteArtist({ - id: props.artistId, - deleteFiles, - addImportListExclusion - }) - ); +class DeleteArtistModalContentConnector extends Component { - props.onModalClose(true); - } + // + // Listeners + + onDeletePress = (deleteFiles, addImportListExclusion) => { + this.props.deleteArtist({ + id: this.props.artistId, + deleteFiles, + addImportListExclusion + }); + + this.props.onModalClose(true); }; + + // + // Render + + render() { + return ( + + ); + } } -export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteArtistModalContent); +DeleteArtistModalContentConnector.propTypes = { + artistId: PropTypes.number.isRequired, + onModalClose: PropTypes.func.isRequired, + deleteArtist: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DeleteArtistModalContentConnector); diff --git a/frontend/src/Artist/Details/AlbumGroupInfo.js b/frontend/src/Artist/Details/AlbumGroupInfo.js index 139cd7765..0fb62d4a3 100644 --- a/frontend/src/Artist/Details/AlbumGroupInfo.js +++ b/frontend/src/Artist/Details/AlbumGroupInfo.js @@ -10,7 +10,6 @@ function AlbumGroupInfo(props) { const { totalAlbumCount, monitoredAlbumCount, - albumFileCount, trackFileCount, sizeOnDisk } = props; @@ -31,13 +30,6 @@ function AlbumGroupInfo(props) { data={monitoredAlbumCount} /> - - - {secondaryTypes.join(', ')} + { + secondaryTypes + }
); } @@ -158,7 +158,7 @@ class AlbumRow extends Component { return ( { - totalTrackCount + statistics.totalTrackCount } ); @@ -196,17 +196,6 @@ class AlbumRow extends Component { ); } - if (name === 'size') { - return ( - - {!!sizeOnDisk && formatBytes(sizeOnDisk)} - - ); - } - if (name === 'status') { return ( { - this.setState({ isMonitorOptionsModalOpen: true }); - }; - - onMonitorOptionsClose = () => { - this.setState({ isMonitorOptionsModalOpen: false }); - }; - onExpandAllPress = () => { const { allExpanded, @@ -192,7 +185,7 @@ class ArtistDetails extends Component { artistName, ratings, path, - statistics = {}, + statistics, qualityProfileId, monitored, genres, @@ -222,8 +215,8 @@ class ArtistDetails extends Component { } = this.props; const { - trackFileCount = 0, - sizeOnDisk = 0 + trackFileCount, + sizeOnDisk } = statistics; const { @@ -235,14 +228,13 @@ class ArtistDetails extends Component { isArtistHistoryModalOpen, isInteractiveImportModalOpen, isInteractiveSearchModalOpen, - isMonitorOptionsModalOpen, allExpanded, allCollapsed, expandedState } = this.state; const continuing = status === 'continuing'; - const endedString = artistType === 'Person' ? translate('Deceased') : translate('Inactive'); + const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive'; let trackFilesCountMessage = translate('TrackFilesCountMessage'); @@ -331,12 +323,6 @@ class ArtistDetails extends Component { - - - @@ -525,7 +510,7 @@ class ArtistDetails extends Component { /> - {monitored ? translate('Monitored') : translate('Unmonitored')} + {monitored ? 'Monitored' : 'Unmonitored'} @@ -540,7 +525,7 @@ class ArtistDetails extends Component { /> - {continuing ? translate('Continuing') : endedString} + {continuing ? 'Continuing' : endedString} @@ -556,7 +541,7 @@ class ArtistDetails extends Component { /> - {translate('Links')} + Links } @@ -612,19 +597,17 @@ class ArtistDetails extends Component { } { - !isFetching && albumsError ? - - {translate('AlbumsLoadError')} - : - null + !isFetching && albumsError && +
+ {translate('LoadingAlbumsFailed')} +
} { - !isFetching && trackFilesError ? - - {translate('TrackFilesLoadError')} - : - null + !isFetching && trackFilesError && +
+ {translate('LoadingTrackFilesFailed')} +
} { @@ -710,12 +693,6 @@ class ArtistDetails extends Component { artistId={id} onModalClose={this.onInteractiveSearchModalClose} /> - - ); diff --git a/frontend/src/Artist/Details/ArtistDetailsConnector.js b/frontend/src/Artist/Details/ArtistDetailsConnector.js index bed30a937..cd2a911c5 100644 --- a/frontend/src/Artist/Details/ArtistDetailsConnector.js +++ b/frontend/src/Artist/Details/ArtistDetailsConnector.js @@ -107,6 +107,7 @@ function createMapStateToProps() { const isRefreshing = isArtistRefreshing || allArtistRefreshing; const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id })); const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id })); + const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST }); const isRenamingArtist = ( isCommandExecuting(isRenamingArtistCommand) && diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.js b/frontend/src/Artist/Details/ArtistDetailsSeason.js index 004613e30..9af3f8a17 100644 --- a/frontend/src/Artist/Details/ArtistDetailsSeason.js +++ b/frontend/src/Artist/Details/ArtistDetailsSeason.js @@ -20,45 +20,34 @@ import AlbumGroupInfo from './AlbumGroupInfo'; import AlbumRowConnector from './AlbumRowConnector'; import styles from './ArtistDetailsSeason.css'; -function getAlbumStatistics(albums) { +function getAlbumTypeStatistics(albums) { let albumCount = 0; - let albumFileCount = 0; let trackFileCount = 0; let totalAlbumCount = 0; let monitoredAlbumCount = 0; let hasMonitoredAlbums = false; let sizeOnDisk = 0; - albums.forEach(({ monitored, releaseDate, statistics = {} }) => { - const { - trackFileCount: albumTrackFileCount = 0, - totalTrackCount: albumTotalTrackCount = 0, - sizeOnDisk: albumSizeOnDisk = 0 - } = statistics; + albums.forEach((album) => { + if (album.statistics) { + sizeOnDisk = sizeOnDisk + album.statistics.sizeOnDisk; + trackFileCount = trackFileCount + album.statistics.trackFileCount; - const hasFiles = albumTrackFileCount > 0 && albumTrackFileCount === albumTotalTrackCount; - - if (hasFiles || (monitored && isBefore(releaseDate))) { - albumCount++; + if (album.statistics.trackFileCount === album.statistics.totalTrackCount || (album.monitored && isBefore(album.airDateUtc))) { + albumCount++; + } } - if (hasFiles) { - albumFileCount++; - } - - if (monitored) { + if (album.monitored) { monitoredAlbumCount++; hasMonitoredAlbums = true; } totalAlbumCount++; - trackFileCount = trackFileCount + albumTrackFileCount; - sizeOnDisk = sizeOnDisk + albumSizeOnDisk; }); return { albumCount, - albumFileCount, totalAlbumCount, trackFileCount, monitoredAlbumCount, @@ -67,8 +56,8 @@ function getAlbumStatistics(albums) { }; } -function getAlbumCountKind(monitored, albumCount, albumFileCount) { - if (albumCount === albumFileCount && albumFileCount > 0) { +function getAlbumCountKind(monitored, albumCount, monitoredAlbumCount) { + if (albumCount === monitoredAlbumCount && monitoredAlbumCount > 0) { return kinds.SUCCESS; } @@ -203,13 +192,12 @@ class ArtistDetailsSeason extends Component { const { albumCount, - albumFileCount, totalAlbumCount, trackFileCount, monitoredAlbumCount, - hasMonitoredAlbums, - sizeOnDisk = 0 - } = getAlbumStatistics(items); + sizeOnDisk, + hasMonitoredAlbums + } = getAlbumTypeStatistics(items); const { isOrganizeModalOpen, @@ -238,9 +226,9 @@ class ArtistDetailsSeason extends Component { anchor={ } title={translate('GroupInformation')} @@ -249,7 +237,6 @@ class ArtistDetailsSeason extends Component { diff --git a/frontend/src/Artist/Details/ArtistTagsConnector.js b/frontend/src/Artist/Details/ArtistTagsConnector.js index 1d24a5755..33ced5f0d 100644 --- a/frontend/src/Artist/Details/ArtistTagsConnector.js +++ b/frontend/src/Artist/Details/ArtistTagsConnector.js @@ -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 diff --git a/frontend/src/Artist/Edit/EditArtistModal.js b/frontend/src/Artist/Edit/EditArtistModal.js index f221e728c..6e99a2f53 100644 --- a/frontend/src/Artist/Edit/EditArtistModal.js +++ b/frontend/src/Artist/Edit/EditArtistModal.js @@ -18,7 +18,6 @@ function EditArtistModal({ isOpen, onModalClose, ...otherProps }) { } EditArtistModal.propTypes = { - ...EditArtistModalContentConnector.propTypes, isOpen: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Edit/EditArtistModalConnector.js b/frontend/src/Artist/Edit/EditArtistModalConnector.js index 9c4e6325f..56e336201 100644 --- a/frontend/src/Artist/Edit/EditArtistModalConnector.js +++ b/frontend/src/Artist/Edit/EditArtistModalConnector.js @@ -32,7 +32,6 @@ class EditArtistModalConnector extends Component { } EditArtistModalConnector.propTypes = { - ...EditArtistModal.propTypes, onModalClose: PropTypes.func.isRequired, clearPendingChanges: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.js b/frontend/src/Artist/Edit/EditArtistModalContent.js index bca6e3ea6..c919aec88 100644 --- a/frontend/src/Artist/Edit/EditArtistModalContent.js +++ b/frontend/src/Artist/Edit/EditArtistModalContent.js @@ -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'; @@ -35,10 +35,6 @@ class EditArtistModalContent extends Component { // // Listeners - onCancelPress = () => { - this.setState({ isConfirmMoveModalOpen: false }); - }; - onSavePress = () => { const { isPathChanging, @@ -93,7 +89,7 @@ class EditArtistModalContent extends Component {
- + {translate('Monitored')} @@ -107,10 +103,9 @@ class EditArtistModalContent extends Component { /> - + {translate('MonitorNewItems')} - - + {translate('QualityProfile')} @@ -147,10 +142,10 @@ class EditArtistModalContent extends Component { { - showMetadataProfile ? - + showMetadataProfile && + - {translate('MetadataProfile')} + Metadata Profile - : - null + } - + {translate('Path')} @@ -191,7 +185,7 @@ class EditArtistModalContent extends Component { /> - + {translate('Tags')} @@ -211,7 +205,7 @@ class EditArtistModalContent extends Component { kind={kinds.DANGER} onPress={onDeleteArtistPress} > - {translate('Delete')} + Delete + + + + + ); +} + +RetagArtistModalContent.propTypes = { + artistNames: PropTypes.arrayOf(PropTypes.string).isRequired, + onModalClose: PropTypes.func.isRequired, + onRetagArtistPress: PropTypes.func.isRequired +}; + +export default RetagArtistModalContent; diff --git a/frontend/src/Artist/Editor/AudioTags/RetagArtistModalContentConnector.js b/frontend/src/Artist/Editor/AudioTags/RetagArtistModalContentConnector.js new file mode 100644 index 000000000..83e9fc06d --- /dev/null +++ b/frontend/src/Artist/Editor/AudioTags/RetagArtistModalContentConnector.js @@ -0,0 +1,67 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as commandNames from 'Commands/commandNames'; +import { executeCommand } from 'Store/Actions/commandActions'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import RetagArtistModalContent from './RetagArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + (artistIds, allArtists) => { + const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + return s.id === id; + }); + + const sortedArtist = _.orderBy(artist, 'sortName'); + const artistNames = _.map(sortedArtist, 'artistName'); + + return { + artistNames + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class RetagArtistModalContentConnector extends Component { + + // + // Listeners + + onRetagArtistPress = () => { + this.props.executeCommand({ + name: commandNames.RETAG_ARTIST, + artistIds: this.props.artistIds + }); + + this.props.onModalClose(true); + }; + + // + // Render + + render(props) { + return ( + + ); + } +} + +RetagArtistModalContentConnector.propTypes = { + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + onModalClose: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(RetagArtistModalContentConnector); diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js new file mode 100644 index 000000000..11fd79d5d --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import DeleteArtistModalContentConnector from './DeleteArtistModalContentConnector'; + +function DeleteArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +DeleteArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteArtistModal; diff --git a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.css b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css similarity index 100% rename from frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.css rename to frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css diff --git a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.css.d.ts b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css.d.ts similarity index 100% rename from frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.css.d.ts rename to frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css.d.ts diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js new file mode 100644 index 000000000..6e04fc7e6 --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './DeleteArtistModalContent.css'; + +class DeleteArtistModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + deleteFiles: false + }; + } + + // + // Listeners + + onDeleteFilesChange = ({ value }) => { + this.setState({ deleteFiles: value }); + }; + + onDeleteArtistConfirmed = () => { + const deleteFiles = this.state.deleteFiles; + + this.setState({ deleteFiles: false }); + this.props.onDeleteSelectedPress(deleteFiles); + }; + + // + // Render + + render() { + const { + artist, + onModalClose + } = this.props; + const deleteFiles = this.state.deleteFiles; + + return ( + + + {translate('DeleteArtist')} + + + +
+ + {`Delete Artist Folder${artist.length > 1 ? 's' : ''}`} + + 1 ? 's' : ''} and all contents`} + kind={kinds.DANGER} + onChange={this.onDeleteFilesChange} + /> + +
+ +
+ {`Are you sure you want to delete ${artist.length} selected artist${artist.length > 1 ? 's' : ''}${deleteFiles ? ' and all contents' : ''}?`} +
+ +
    + { + artist.map((s) => { + return ( +
  • + {s.artistName} + + { + deleteFiles && + + - + + {s.path} + + + } +
  • + ); + }) + } +
+
+ + + + + + +
+ ); + } +} + +DeleteArtistModalContent.propTypes = { + artist: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteSelectedPress: PropTypes.func.isRequired +}; + +export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js new file mode 100644 index 000000000..e3b0fa0fd --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js @@ -0,0 +1,45 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { bulkDeleteArtist } from 'Store/Actions/artistEditorActions'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import DeleteArtistModalContent from './DeleteArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + (artistIds, allArtists) => { + const selectedArtist = _.intersectionWith(allArtists, artistIds, (s, id) => { + return s.id === id; + }); + + const sortedArtist = _.orderBy(selectedArtist, 'sortName'); + const artist = _.map(sortedArtist, (s) => { + return { + artistName: s.artistName, + path: s.path + }; + }); + + return { + artist + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onDeleteSelectedPress(deleteFiles) { + dispatch(bulkDeleteArtist({ + artistIds: props.artistIds, + deleteFiles + })); + + props.onModalClose(); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteArtistModalContent); diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModal.js b/frontend/src/Artist/Editor/Organize/OrganizeArtistModal.js new file mode 100644 index 000000000..412396355 --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import OrganizeArtistModalContentConnector from './OrganizeArtistModalContentConnector'; + +function OrganizeArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +OrganizeArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default OrganizeArtistModal; diff --git a/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModalContent.css b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.css similarity index 100% rename from frontend/src/Artist/Index/Select/Organize/OrganizeArtistModalContent.css rename to frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.css diff --git a/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModalContent.css.d.ts b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.css.d.ts similarity index 100% rename from frontend/src/Artist/Index/Select/Organize/OrganizeArtistModalContent.css.d.ts rename to frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.css.d.ts diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.js b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.js new file mode 100644 index 000000000..30a6929cd --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { icons, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './OrganizeArtistModalContent.css'; + +function OrganizeArtistModalContent(props) { + const { + artistNames, + onModalClose, + onOrganizeArtistPress + } = props; + + return ( + + + {translate('OrganizeArtist')} + + + + + Tip: To preview a rename, select "Cancel", then select any artist name and use the + + + +
+ Are you sure you want to organize all files in the {artistNames.length} selected artist? +
+ +
    + { + artistNames.map((artistName) => { + return ( +
  • + {artistName} +
  • + ); + }) + } +
+
+ + + + + + +
+ ); +} + +OrganizeArtistModalContent.propTypes = { + artistNames: PropTypes.arrayOf(PropTypes.string).isRequired, + onModalClose: PropTypes.func.isRequired, + onOrganizeArtistPress: PropTypes.func.isRequired +}; + +export default OrganizeArtistModalContent; diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js new file mode 100644 index 000000000..3fad07da0 --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js @@ -0,0 +1,67 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as commandNames from 'Commands/commandNames'; +import { executeCommand } from 'Store/Actions/commandActions'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import OrganizeArtistModalContent from './OrganizeArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + (artistIds, allArtists) => { + const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + return s.id === id; + }); + + const sortedArtist = _.orderBy(artist, 'sortName'); + const artistNames = _.map(sortedArtist, 'artistName'); + + return { + artistNames + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class OrganizeArtistModalContentConnector extends Component { + + // + // Listeners + + onOrganizeArtistPress = () => { + this.props.executeCommand({ + name: commandNames.RENAME_ARTIST, + artistIds: this.props.artistIds + }); + + this.props.onModalClose(true); + }; + + // + // Render + + render(props) { + return ( + + ); + } +} + +OrganizeArtistModalContentConnector.propTypes = { + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + onModalClose: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeArtistModalContentConnector); diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.js b/frontend/src/Artist/Editor/Tags/TagsModal.js similarity index 58% rename from frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.js rename to frontend/src/Artist/Editor/Tags/TagsModal.js index 1a8c115f0..0f6c2d7ec 100644 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.js +++ b/frontend/src/Artist/Editor/Tags/TagsModal.js @@ -1,15 +1,21 @@ import PropTypes from 'prop-types'; import React from 'react'; import Modal from 'Components/Modal/Modal'; -import AddSpecificationModalContent from './AddSpecificationModalContent'; +import TagsModalContentConnector from './TagsModalContentConnector'; + +function TagsModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; -function AddSpecificationModal({ isOpen, onModalClose, ...otherProps }) { return ( - @@ -17,9 +23,9 @@ function AddSpecificationModal({ isOpen, onModalClose, ...otherProps }) { ); } -AddSpecificationModal.propTypes = { +TagsModal.propTypes = { isOpen: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired }; -export default AddSpecificationModal; +export default TagsModal; diff --git a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.css b/frontend/src/Artist/Editor/Tags/TagsModalContent.css similarity index 100% rename from frontend/src/Artist/Index/Select/Tags/TagsModalContent.css rename to frontend/src/Artist/Editor/Tags/TagsModalContent.css diff --git a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.css.d.ts b/frontend/src/Artist/Editor/Tags/TagsModalContent.css.d.ts similarity index 100% rename from frontend/src/Artist/Index/Select/Tags/TagsModalContent.css.d.ts rename to frontend/src/Artist/Editor/Tags/TagsModalContent.css.d.ts diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContent.js b/frontend/src/Artist/Editor/Tags/TagsModalContent.js new file mode 100644 index 000000000..5245ba097 --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContent.js @@ -0,0 +1,194 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Label from 'Components/Label'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './TagsModalContent.css'; + +class TagsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + tags: [], + applyTags: 'add' + }; + } + + // + // Lifecycle + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + }; + + onApplyTagsPress = () => { + const { + tags, + applyTags + } = this.state; + + this.props.onApplyTagsPress(tags, applyTags); + }; + + // + // Render + + render() { + const { + artistTags, + tagList, + onModalClose + } = this.props; + + const { + tags, + applyTags + } = this.state; + + const applyTagsOptions = [ + { key: 'add', value: translate('Add') }, + { key: 'remove', value: translate('Remove') }, + { key: 'replace', value: translate('Replace') } + ]; + + return ( + + + Tags + + + + + + + {translate('Tags')} + + + + + + + + {translate('ApplyTags')} + + + + + + + + {translate('Result')} + + +
+ { + artistTags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + const removeTag = (applyTags === 'remove' && tags.indexOf(t) > -1) || + (applyTags === 'replace' && tags.indexOf(t) === -1); + + return ( + + ); + }) + } + + { + (applyTags === 'add' || applyTags === 'replace') && + tags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + if (artistTags.indexOf(t) > -1) { + return null; + } + + return ( + + ); + }) + } +
+
+ +
+ + + + + + +
+ ); + } +} + +TagsModalContent.propTypes = { + artistTags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired, + onApplyTagsPress: PropTypes.func.isRequired +}; + +export default TagsModalContent; diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js new file mode 100644 index 000000000..6741e8b5c --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import TagsModalContent from './TagsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + createTagsSelector(), + (artistIds, allArtists, tagList) => { + const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + return s.id === id; + }); + + const artistTags = _.uniq(_.concat(..._.map(artist, 'tags'))); + + return { + artistTags, + tagList + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onAction() { + // Do something + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(TagsModalContent); diff --git a/frontend/src/Artist/History/ArtistHistoryModal.js b/frontend/src/Artist/History/ArtistHistoryModal.js index a4362484c..7139d7633 100644 --- a/frontend/src/Artist/History/ArtistHistoryModal.js +++ b/frontend/src/Artist/History/ArtistHistoryModal.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector'; function ArtistHistoryModal(props) { @@ -14,7 +13,6 @@ function ArtistHistoryModal(props) { return ( translate('CustomFormats'), - isSortable: false, + name: 'date', + label: () => translate('Date'), + isVisible: true + }, + { + name: 'details', + label: () => translate('Details'), isVisible: true }, { @@ -49,13 +53,9 @@ const columns = [ isSortable: true, isVisible: true }, - { - name: 'date', - label: () => translate('Date'), - isVisible: true - }, { name: 'actions', + label: () => translate('Actions'), isVisible: true } ]; diff --git a/frontend/src/Artist/History/ArtistHistoryRow.css b/frontend/src/Artist/History/ArtistHistoryRow.css index 33dba8df9..a62b9c2bb 100644 --- a/frontend/src/Artist/History/ArtistHistoryRow.css +++ b/frontend/src/Artist/History/ArtistHistoryRow.css @@ -4,6 +4,7 @@ word-break: break-word; } +.details, .actions { composes: cell from '~Components/Table/Cells/TableRowCell.css'; diff --git a/frontend/src/Artist/History/ArtistHistoryRow.css.d.ts b/frontend/src/Artist/History/ArtistHistoryRow.css.d.ts index b0b91a6b8..e3425487e 100644 --- a/frontend/src/Artist/History/ArtistHistoryRow.css.d.ts +++ b/frontend/src/Artist/History/ArtistHistoryRow.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'actions': string; + 'details': string; 'sourceTitle': string; } export const cssExports: CssExports; diff --git a/frontend/src/Artist/History/ArtistHistoryRow.js b/frontend/src/Artist/History/ArtistHistoryRow.js index fe8326378..d48e2eb31 100644 --- a/frontend/src/Artist/History/ArtistHistoryRow.js +++ b/frontend/src/Artist/History/ArtistHistoryRow.js @@ -11,6 +11,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import Popover from 'Components/Tooltip/Popover'; +import Tooltip from 'Components/Tooltip/Tooltip'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import translate from 'Utilities/String/translate'; @@ -81,7 +82,6 @@ class ArtistHistoryRow extends Component { customFormatScore, date, data, - downloadId, album } = this.props; @@ -111,19 +111,11 @@ class ArtistHistoryRow extends Component { />
- - - - - - {formatCustomFormatScore(customFormatScore, customFormats.length)} - - - + } position={tooltipPositions.LEFT} /> + + + } + position={tooltipPositions.BOTTOM} + /> + + + { eventType === 'grabbed' && } @@ -177,7 +180,6 @@ ArtistHistoryRow.propTypes = { customFormatScore: PropTypes.number.isRequired, date: PropTypes.string.isRequired, data: PropTypes.object.isRequired, - downloadId: PropTypes.string, fullArtist: PropTypes.bool.isRequired, artist: PropTypes.object.isRequired, album: PropTypes.object.isRequired, diff --git a/frontend/src/Artist/Index/ArtistIndex.css b/frontend/src/Artist/Index/ArtistIndex.css index 908cb2d16..43b445c3c 100644 --- a/frontend/src/Artist/Index/ArtistIndex.css +++ b/frontend/src/Artist/Index/ArtistIndex.css @@ -13,7 +13,6 @@ .contentBody { composes: contentBody from '~Components/Page/PageContentBody.css'; - position: relative; display: flex; flex-direction: column; } diff --git a/frontend/src/Artist/Index/ArtistIndex.js b/frontend/src/Artist/Index/ArtistIndex.js new file mode 100644 index 000000000..6f68f7fcd --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndex.js @@ -0,0 +1,407 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import NoArtist from 'Artist/NoArtist'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageJumpBar from 'Components/Page/PageJumpBar'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; +import { align, icons, sortDirections } from 'Helpers/Props'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; +import translate from 'Utilities/String/translate'; +import ArtistIndexFooterConnector from './ArtistIndexFooterConnector'; +import ArtistIndexBannersConnector from './Banners/ArtistIndexBannersConnector'; +import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal'; +import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu'; +import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu'; +import ArtistIndexViewMenu from './Menus/ArtistIndexViewMenu'; +import ArtistIndexOverviewsConnector from './Overview/ArtistIndexOverviewsConnector'; +import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal'; +import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector'; +import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal'; +import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector'; +import ArtistIndexTableOptionsConnector from './Table/ArtistIndexTableOptionsConnector'; +import styles from './ArtistIndex.css'; + +function getViewComponent(view) { + if (view === 'posters') { + return ArtistIndexPostersConnector; + } + + if (view === 'banners') { + return ArtistIndexBannersConnector; + } + + if (view === 'overview') { + return ArtistIndexOverviewsConnector; + } + + return ArtistIndexTableConnector; +} + +class ArtistIndex extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + scroller: null, + jumpBarItems: { order: [] }, + jumpToCharacter: null, + isPosterOptionsModalOpen: false, + isBannerOptionsModalOpen: false, + isOverviewOptionsModalOpen: false + }; + } + + componentDidMount() { + this.setJumpBarItems(); + } + + componentDidUpdate(prevProps) { + const { + items, + sortKey, + sortDirection + } = this.props; + + if (sortKey !== prevProps.sortKey || + sortDirection !== prevProps.sortDirection || + hasDifferentItemsOrOrder(prevProps.items, items) + ) { + this.setJumpBarItems(); + } + + if (this.state.jumpToCharacter != null) { + this.setState({ jumpToCharacter: null }); + } + } + + // + // Control + + setScrollerRef = (ref) => { + this.setState({ scroller: ref }); + }; + + setJumpBarItems() { + const { + items, + sortKey, + sortDirection + } = this.props; + + // Reset if not sorting by sortName + if (sortKey !== 'sortName') { + this.setState({ jumpBarItems: { order: [] } }); + return; + } + + const characters = _.reduce(items, (acc, item) => { + let char = item.sortName.charAt(0); + + if (!isNaN(char)) { + char = '#'; + } + + if (char in acc) { + acc[char] = acc[char] + 1; + } else { + acc[char] = 1; + } + + return acc; + }, {}); + + const order = Object.keys(characters).sort(); + + // Reverse if sorting descending + if (sortDirection === sortDirections.DESCENDING) { + order.reverse(); + } + + const jumpBarItems = { + characters, + order + }; + + this.setState({ jumpBarItems }); + } + + // + // Listeners + + onPosterOptionsPress = () => { + this.setState({ isPosterOptionsModalOpen: true }); + }; + + onPosterOptionsModalClose = () => { + this.setState({ isPosterOptionsModalOpen: false }); + }; + + onBannerOptionsPress = () => { + this.setState({ isBannerOptionsModalOpen: true }); + }; + + onBannerOptionsModalClose = () => { + this.setState({ isBannerOptionsModalOpen: false }); + }; + + onOverviewOptionsPress = () => { + this.setState({ isOverviewOptionsModalOpen: true }); + }; + + onOverviewOptionsModalClose = () => { + this.setState({ isOverviewOptionsModalOpen: false }); + }; + + onJumpBarItemPress = (jumpToCharacter) => { + this.setState({ jumpToCharacter }); + }; + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + totalItems, + items, + columns, + selectedFilterKey, + filters, + customFilters, + sortKey, + sortDirection, + view, + isRefreshingArtist, + isRssSyncExecuting, + onScroll, + onSortSelect, + onFilterSelect, + onViewSelect, + onRefreshArtistPress, + onRssSyncPress, + ...otherProps + } = this.props; + + const { + scroller, + jumpBarItems, + jumpToCharacter, + isPosterOptionsModalOpen, + isBannerOptionsModalOpen, + isOverviewOptionsModalOpen + } = this.state; + + const ViewComponent = getViewComponent(view); + const isLoaded = !!(!error && isPopulated && items.length && scroller); + const hasNoArtist = !totalItems; + + return ( + + + + + + + + + + + { + view === 'table' ? + + + : + null + } + + { + view === 'posters' ? + : + null + } + + { + view === 'banners' ? + : + null + } + + { + view === 'overview' ? + : + null + } + + + + + + + + + + + +
+ + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
+ {getErrorMessage(error, 'Failed to load artist from API')} +
+ } + + { + isLoaded && +
+ + + +
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + { + isLoaded && !!jumpBarItems.order.length && + + } +
+ + + + + + +
+ ); + } +} + +ArtistIndex.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + totalItems: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + view: PropTypes.string.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + isRssSyncExecuting: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onViewSelect: PropTypes.func.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired, + onRssSyncPress: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndex; diff --git a/frontend/src/Artist/Index/ArtistIndex.tsx b/frontend/src/Artist/Index/ArtistIndex.tsx deleted file mode 100644 index 2fcc0fadf..000000000 --- a/frontend/src/Artist/Index/ArtistIndex.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { SelectProvider } from 'App/SelectContext'; -import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState'; -import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; -import NoArtist from 'Artist/NoArtist'; -import { RSS_SYNC } from 'Commands/commandNames'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageJumpBar from 'Components/Page/PageJumpBar'; -import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; -import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import withScrollPosition from 'Components/withScrollPosition'; -import { align, icons } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; -import { - setArtistFilter, - setArtistSort, - setArtistTableOption, - setArtistView, -} from 'Store/Actions/artistIndexActions'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchQueueDetails } from 'Store/Actions/queueActions'; -import scrollPositions from 'Store/scrollPositions'; -import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import ArtistIndexFooter from './ArtistIndexFooter'; -import ArtistIndexRefreshArtistsButton from './ArtistIndexRefreshArtistsButton'; -import ArtistIndexBanners from './Banners/ArtistIndexBanners'; -import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal'; -import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu'; -import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu'; -import ArtistIndexViewMenu from './Menus/ArtistIndexViewMenu'; -import ArtistIndexOverviews from './Overview/ArtistIndexOverviews'; -import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal'; -import ArtistIndexPosters from './Posters/ArtistIndexPosters'; -import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal'; -import ArtistIndexSelectAllButton from './Select/ArtistIndexSelectAllButton'; -import ArtistIndexSelectAllMenuItem from './Select/ArtistIndexSelectAllMenuItem'; -import ArtistIndexSelectFooter from './Select/ArtistIndexSelectFooter'; -import ArtistIndexSelectModeButton from './Select/ArtistIndexSelectModeButton'; -import ArtistIndexSelectModeMenuItem from './Select/ArtistIndexSelectModeMenuItem'; -import ArtistIndexTable from './Table/ArtistIndexTable'; -import ArtistIndexTableOptions from './Table/ArtistIndexTableOptions'; -import styles from './ArtistIndex.css'; - -function getViewComponent(view: string) { - if (view === 'posters') { - return ArtistIndexPosters; - } - - if (view === 'banners') { - return ArtistIndexBanners; - } - - if (view === 'overview') { - return ArtistIndexOverviews; - } - - return ArtistIndexTable; -} - -interface ArtistIndexProps { - initialScrollTop?: number; -} - -const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => { - const { - isFetching, - isPopulated, - error, - totalItems, - items, - columns, - selectedFilterKey, - filters, - customFilters, - sortKey, - sortDirection, - view, - }: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState = - useSelector(createArtistClientSideCollectionItemsSelector('artistIndex')); - - const isRssSyncExecuting = useSelector( - createCommandExecutingSelector(RSS_SYNC) - ); - const { isSmallScreen } = useSelector(createDimensionsSelector()); - const dispatch = useDispatch(); - const scrollerRef = useRef(null); - const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false); - const [jumpToCharacter, setJumpToCharacter] = useState( - undefined - ); - const [isSelectMode, setIsSelectMode] = useState(false); - - useEffect(() => { - dispatch(fetchQueueDetails({ all: true })); - }, [dispatch]); - - const onRssSyncPress = useCallback(() => { - dispatch( - executeCommand({ - name: RSS_SYNC, - }) - ); - }, [dispatch]); - - const onSelectModePress = useCallback(() => { - setIsSelectMode(!isSelectMode); - }, [isSelectMode, setIsSelectMode]); - - const onTableOptionChange = useCallback( - (payload: unknown) => { - dispatch(setArtistTableOption(payload)); - }, - [dispatch] - ); - - const onViewSelect = useCallback( - (value: string) => { - dispatch(setArtistView({ view: value })); - - if (scrollerRef.current) { - scrollerRef.current.scrollTo(0, 0); - } - }, - [scrollerRef, dispatch] - ); - - const onSortSelect = useCallback( - (value: string) => { - dispatch(setArtistSort({ sortKey: value })); - }, - [dispatch] - ); - - const onFilterSelect = useCallback( - (value: string) => { - dispatch(setArtistFilter({ selectedFilterKey: value })); - }, - [dispatch] - ); - - const onOptionsPress = useCallback(() => { - setIsOptionsModalOpen(true); - }, [setIsOptionsModalOpen]); - - const onOptionsModalClose = useCallback(() => { - setIsOptionsModalOpen(false); - }, [setIsOptionsModalOpen]); - - const onJumpBarItemPress = useCallback( - (character: string) => { - setJumpToCharacter(character); - }, - [setJumpToCharacter] - ); - - const onScroll = useCallback( - ({ scrollTop }: { scrollTop: number }) => { - setJumpToCharacter(undefined); - scrollPositions.artistIndex = scrollTop; - }, - [setJumpToCharacter] - ); - - const jumpBarItems = useMemo(() => { - // Reset if not sorting by sortName - if (sortKey !== 'sortName') { - return { - order: [], - }; - } - - const characters = items.reduce((acc: Record, item) => { - let char = item.sortName.charAt(0); - - if (!isNaN(Number(char))) { - char = '#'; - } - - if (char in acc) { - acc[char] = acc[char] + 1; - } else { - acc[char] = 1; - } - - return acc; - }, {}); - - const order = Object.keys(characters).sort(); - - // Reverse if sorting descending - if (sortDirection === SortDirection.Descending) { - order.reverse(); - } - - return { - characters, - order, - }; - }, [items, sortKey, sortDirection]); - const ViewComponent = useMemo(() => getViewComponent(view), [view]); - - const isLoaded = !!(!error && isPopulated && items.length); - const hasNoArtist = !totalItems; - - return ( - - - - - - - - - - - - - - - - - {view === 'table' ? ( - - - - ) : ( - - )} - - - - - - - - - - -
- - {isFetching && !isPopulated ? : null} - - {!isFetching && !!error ? ( -
- {getErrorMessage(error, 'Failed to load artist from API')} -
- ) : null} - - {isLoaded ? ( -
- - - -
- ) : null} - - {!error && isPopulated && !items.length ? ( - - ) : null} -
- {isLoaded && !!jumpBarItems.order.length ? ( - - ) : null} -
- - {isSelectMode ? : null} - - {view === 'posters' ? ( - - ) : null} - {view === 'banners' ? ( - - ) : null} - {view === 'overview' ? ( - - ) : null} -
-
- ); -}, 'artistIndex'); - -export default ArtistIndex; diff --git a/frontend/src/Artist/Index/ArtistIndexConnector.js b/frontend/src/Artist/Index/ArtistIndexConnector.js new file mode 100644 index 000000000..541d9819e --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexConnector.js @@ -0,0 +1,106 @@ +/* eslint max-params: 0 */ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as commandNames from 'Commands/commandNames'; +import withScrollPosition from 'Components/withScrollPosition'; +import { setArtistFilter, setArtistSort, setArtistTableOption, setArtistView } from 'Store/Actions/artistIndexActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import scrollPositions from 'Store/scrollPositions'; +import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import ArtistIndex from './ArtistIndex'; + +function createMapStateToProps() { + return createSelector( + createArtistClientSideCollectionItemsSelector('artistIndex'), + createCommandExecutingSelector(commandNames.REFRESH_ARTIST), + createCommandExecutingSelector(commandNames.RSS_SYNC), + createDimensionsSelector(), + ( + artist, + isRefreshingArtist, + isRssSyncExecuting, + dimensionsState + ) => { + return { + ...artist, + isRefreshingArtist, + isRssSyncExecuting, + isSmallScreen: dimensionsState.isSmallScreen + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onTableOptionChange(payload) { + dispatch(setArtistTableOption(payload)); + }, + + onSortSelect(sortKey) { + dispatch(setArtistSort({ sortKey })); + }, + + onFilterSelect(selectedFilterKey) { + dispatch(setArtistFilter({ selectedFilterKey })); + }, + + dispatchSetArtistView(view) { + dispatch(setArtistView({ view })); + }, + + onRefreshArtistPress() { + dispatch(executeCommand({ + name: commandNames.REFRESH_ARTIST + })); + }, + + onRssSyncPress() { + dispatch(executeCommand({ + name: commandNames.RSS_SYNC + })); + } + }; +} + +class ArtistIndexConnector extends Component { + + // + // Listeners + + onViewSelect = (view) => { + this.props.dispatchSetArtistView(view); + }; + + onScroll = ({ scrollTop }) => { + scrollPositions.artistIndex = scrollTop; + }; + + // + // Render + + render() { + return ( + + ); + } +} + +ArtistIndexConnector.propTypes = { + isSmallScreen: PropTypes.bool.isRequired, + view: PropTypes.string.isRequired, + dispatchSetArtistView: PropTypes.func.isRequired +}; + +export default withScrollPosition( + connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexConnector), + 'artistIndex' +); diff --git a/frontend/src/Artist/Index/ArtistIndexFilterModal.tsx b/frontend/src/Artist/Index/ArtistIndexFilterModal.tsx deleted file mode 100644 index 07e454fc2..000000000 --- a/frontend/src/Artist/Index/ArtistIndexFilterModal.tsx +++ /dev/null @@ -1,56 +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 { setArtistFilter } from 'Store/Actions/artistIndexActions'; - -function createArtistSelector() { - return createSelector( - (state: AppState) => state.artist.items, - (artist) => { - return artist; - } - ); -} - -function createFilterBuilderPropsSelector() { - return createSelector( - (state: AppState) => state.artistIndex.filterBuilderProps, - (filterBuilderProps) => { - return filterBuilderProps; - } - ); -} - -interface ArtistIndexFilterModalProps { - isOpen: boolean; -} - -export default function ArtistIndexFilterModal( - props: ArtistIndexFilterModalProps -) { - const sectionItems = useSelector(createArtistSelector()); - const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'artist'; - - const dispatch = useDispatch(); - - const dispatchSetFilter = useCallback( - (payload: unknown) => { - dispatch(setArtistFilter(payload)); - }, - [dispatch] - ); - - return ( - - ); -} diff --git a/frontend/src/Artist/Index/ArtistIndexFilterModalConnector.js b/frontend/src/Artist/Index/ArtistIndexFilterModalConnector.js new file mode 100644 index 000000000..cf5ec33ea --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFilterModalConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setArtistFilter } from 'Store/Actions/artistIndexActions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artist.items, + (state) => state.artistIndex.filterBuilderProps, + (sectionItems, filterBuilderProps) => { + return { + sectionItems, + filterBuilderProps, + customFilterType: 'artistIndex' + }; + } + ); +} + +const mapDispatchToProps = { + dispatchSetFilter: setArtistFilter +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal); diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.css b/frontend/src/Artist/Index/ArtistIndexFooter.css index bf3fedfd6..c1c4b5a46 100644 --- a/frontend/src/Artist/Index/ArtistIndexFooter.css +++ b/frontend/src/Artist/Index/ArtistIndexFooter.css @@ -50,12 +50,6 @@ } } -.downloading { - composes: legendItemColor; - - background-color: var(--purple); -} - .statistics { display: flex; justify-content: space-between; diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.css.d.ts b/frontend/src/Artist/Index/ArtistIndexFooter.css.d.ts index 29f693a8c..b88d23a6c 100644 --- a/frontend/src/Artist/Index/ArtistIndexFooter.css.d.ts +++ b/frontend/src/Artist/Index/ArtistIndexFooter.css.d.ts @@ -2,7 +2,6 @@ // Please do not change this file! interface CssExports { 'continuing': string; - 'downloading': string; 'ended': string; 'footer': string; 'legendItem': string; diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.js b/frontend/src/Artist/Index/ArtistIndexFooter.js new file mode 100644 index 000000000..5b0f1fc5a --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFooter.js @@ -0,0 +1,167 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { PureComponent } from 'react'; +import { ColorImpairedConsumer } from 'App/ColorImpairedContext'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; +import styles from './ArtistIndexFooter.css'; + +class ArtistIndexFooter extends PureComponent { + + // + // Render + + render() { + const { artist } = this.props; + const count = artist.length; + let tracks = 0; + let trackFiles = 0; + let ended = 0; + let continuing = 0; + let monitored = 0; + let totalFileSize = 0; + + artist.forEach((s) => { + const { statistics = {} } = s; + + const { + trackCount = 0, + trackFileCount = 0, + sizeOnDisk = 0 + } = statistics; + + tracks += trackCount; + trackFiles += trackFileCount; + + if (s.status === 'ended') { + ended++; + } else { + continuing++; + } + + if (s.monitored) { + monitored++; + } + + totalFileSize += sizeOnDisk; + }); + + return ( + + {(enableColorImpairedMode) => { + return ( +
+
+
+
+
+ {translate('ContinuingAllTracksDownloaded')} +
+
+ +
+
+
+ {translate('EndedAllTracksDownloaded')} +
+
+ +
+
+
+ {translate('MissingTracksArtistMonitored')} +
+
+ +
+
+
+ {translate('MissingTracksArtistNotMonitored')} +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); + }} + + ); + } +} + +ArtistIndexFooter.propTypes = { + artist: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default ArtistIndexFooter; diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.tsx b/frontend/src/Artist/Index/ArtistIndexFooter.tsx deleted file mode 100644 index 47241b224..000000000 --- a/frontend/src/Artist/Index/ArtistIndexFooter.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import { ColorImpairedConsumer } from 'App/ColorImpairedContext'; -import ArtistAppState from 'App/State/ArtistAppState'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector'; -import formatBytes from 'Utilities/Number/formatBytes'; -import translate from 'Utilities/String/translate'; -import styles from './ArtistIndexFooter.css'; - -function createUnoptimizedSelector() { - return createSelector( - createClientSideCollectionSelector('artist', 'artistIndex'), - (artist: ArtistAppState) => { - return artist.items.map((s) => { - const { monitored, status, statistics } = s; - - return { - monitored, - status, - statistics, - }; - }); - } - ); -} - -function createArtistSelector() { - return createDeepEqualSelector( - createUnoptimizedSelector(), - (artist) => artist - ); -} - -export default function ArtistIndexFooter() { - const artist = useSelector(createArtistSelector()); - const count = artist.length; - let tracks = 0; - let trackFiles = 0; - let ended = 0; - let continuing = 0; - let monitored = 0; - let totalFileSize = 0; - - artist.forEach((a) => { - const { statistics = { trackCount: 0, trackFileCount: 0, sizeOnDisk: 0 } } = - a; - - const { trackCount = 0, trackFileCount = 0, sizeOnDisk = 0 } = statistics; - - tracks += trackCount; - trackFiles += trackFileCount; - - if (a.status === 'ended') { - ended++; - } else { - continuing++; - } - - if (a.monitored) { - monitored++; - } - - totalFileSize += sizeOnDisk; - }); - - return ( - - {(enableColorImpairedMode) => { - return ( -
-
-
-
-
{translate('ContinuingAllTracksDownloaded')}
-
- -
-
-
{translate('EndedAllTracksDownloaded')}
-
- -
-
-
{translate('MissingTracksArtistMonitored')}
-
- -
-
-
{translate('MissingTracksArtistNotMonitored')}
-
- -
-
-
{translate('ArtistIndexFooterDownloading')}
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- ); - }} - - ); -} diff --git a/frontend/src/Artist/Index/ArtistIndexFooterConnector.js b/frontend/src/Artist/Index/ArtistIndexFooterConnector.js new file mode 100644 index 000000000..2cb0e3e7d --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFooterConnector.js @@ -0,0 +1,46 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector'; +import ArtistIndexFooter from './ArtistIndexFooter'; + +function createUnoptimizedSelector() { + return createSelector( + createClientSideCollectionSelector('artist', 'artistIndex'), + (artist) => { + return artist.items.map((s) => { + const { + monitored, + status, + statistics + } = s; + + return { + monitored, + status, + statistics + }; + }); + } + ); +} + +function createArtistSelector() { + return createDeepEqualSelector( + createUnoptimizedSelector(), + (artist) => artist + ); +} + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (artist) => { + return { + artist + }; + } + ); +} + +export default connect(createMapStateToProps)(ArtistIndexFooter); diff --git a/frontend/src/Artist/Index/ArtistIndexItemConnector.js b/frontend/src/Artist/Index/ArtistIndexItemConnector.js new file mode 100644 index 000000000..43d92ef13 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexItemConnector.js @@ -0,0 +1,153 @@ +/* eslint max-params: 0 */ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as commandNames from 'Commands/commandNames'; +import { toggleArtistMonitored } from 'Store/Actions/artistActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector'; +import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; + +function selectShowSearchAction() { + return createSelector( + (state) => state.artistIndex, + (artistIndex) => { + const view = artistIndex.view; + + switch (view) { + case 'posters': + return artistIndex.posterOptions.showSearchAction; + case 'banners': + return artistIndex.bannerOptions.showSearchAction; + case 'overview': + return artistIndex.overviewOptions.showSearchAction; + default: + return artistIndex.tableOptions.showSearchAction; + } + } + ); +} + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createArtistQualityProfileSelector(), + createArtistMetadataProfileSelector(), + selectShowSearchAction(), + createExecutingCommandsSelector(), + ( + artist, + qualityProfile, + metadataProfile, + showSearchAction, + 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 === commandNames.REFRESH_ARTIST && + command.body.artistId === artist.id + ); + }); + + const isSearchingArtist = executingCommands.some((command) => { + return ( + command.name === commandNames.ARTIST_SEARCH && + command.body.artistId === artist.id + ); + }); + + const latestAlbum = _.maxBy(artist.albums, (album) => album.releaseDate); + + return { + ...artist, + qualityProfile, + metadataProfile, + latestAlbum, + showSearchAction, + isRefreshingArtist, + isSearchingArtist + }; + } + ); +} + +const mapDispatchToProps = { + dispatchExecuteCommand: executeCommand, + toggleArtistMonitored +}; + +class ArtistIndexItemConnector extends Component { + + // + // Listeners + + onRefreshArtistPress = () => { + this.props.dispatchExecuteCommand({ + name: commandNames.REFRESH_ARTIST, + artistId: this.props.id + }); + }; + + onSearchPress = () => { + this.props.dispatchExecuteCommand({ + name: commandNames.ARTIST_SEARCH, + artistId: this.props.id + }); + }; + + onMonitoredPress = () => { + this.props.toggleArtistMonitored({ + artistId: this.props.id, + monitored: !this.props.monitored + }); + }; + + // + // Render + + render() { + const { + id, + component: ItemComponent, + ...otherProps + } = this.props; + + if (!id) { + return null; + } + + return ( + + ); + } +} + +ArtistIndexItemConnector.propTypes = { + id: PropTypes.number, + monitored: PropTypes.bool.isRequired, + component: PropTypes.elementType.isRequired, + dispatchExecuteCommand: PropTypes.func.isRequired, + toggleArtistMonitored: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexItemConnector); diff --git a/frontend/src/Artist/Index/ArtistIndexRefreshArtistsButton.tsx b/frontend/src/Artist/Index/ArtistIndexRefreshArtistsButton.tsx deleted file mode 100644 index 07a180857..000000000 --- a/frontend/src/Artist/Index/ArtistIndexRefreshArtistsButton.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useSelect } from 'App/SelectContext'; -import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState'; -import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; -import { REFRESH_ARTIST } from 'Commands/commandNames'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import { icons } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; - -interface ArtistIndexRefreshArtistsButtonProps { - isSelectMode: boolean; - selectedFilterKey: string; -} - -function ArtistIndexRefreshArtistsButton( - props: ArtistIndexRefreshArtistsButtonProps -) { - const isRefreshing = useSelector( - createCommandExecutingSelector(REFRESH_ARTIST) - ); - const { - items, - totalItems, - }: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState = - useSelector(createArtistClientSideCollectionItemsSelector('artistIndex')); - - const dispatch = useDispatch(); - const { isSelectMode, selectedFilterKey } = props; - const [selectState] = useSelect(); - const { selectedState } = selectState; - - const selectedArtistIds = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const artistsToRefresh = - isSelectMode && selectedArtistIds.length > 0 - ? selectedArtistIds - : items.map((m) => m.id); - - let refreshLabel = translate('UpdateAll'); - - if (selectedArtistIds.length > 0) { - refreshLabel = translate('UpdateSelected'); - } else if (selectedFilterKey !== 'all') { - refreshLabel = translate('UpdateFiltered'); - } - - const onPress = useCallback(() => { - dispatch( - executeCommand({ - name: REFRESH_ARTIST, - artistIds: artistsToRefresh, - }) - ); - }, [dispatch, artistsToRefresh]); - - return ( - - ); -} - -export default ArtistIndexRefreshArtistsButton; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css index 7f1fc71c6..e22472389 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css @@ -1,5 +1,9 @@ $hoverScale: 1.05; +.container { + padding: 10px; +} + .content { transition: all 200ms ease-in; @@ -22,29 +26,12 @@ $hoverScale: 1.05; .link { composes: link from '~Components/Link/Link.css'; - position: relative; display: block; - height: 50px; background-color: var(--defaultColor); } -.overlayTitle { - position: absolute; - top: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 5px; - width: 100%; - height: 100%; - color: var(--offWhite); - text-align: center; - font-size: 20px; -} - -.nextAlbum { - background-color: var(--artistBackgroundColor); +.nextAiring { + background-color: #fafbfc; text-align: center; font-size: $smallFontSize; } @@ -52,7 +39,8 @@ $hoverScale: 1.05; .title { @add-mixin truncate; - background-color: var(--artistBackgroundColor); + background-color: var(--defaultColor); + color: var(--white); text-align: center; font-size: $smallFontSize; } @@ -61,7 +49,6 @@ $hoverScale: 1.05; position: absolute; top: 0; right: 0; - z-index: 1; width: 0; height: 0; border-width: 0 25px 25px 0; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css.d.ts b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css.d.ts index bd6cb4ac9..393757652 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css.d.ts +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css.d.ts @@ -8,8 +8,7 @@ interface CssExports { 'controls': string; 'ended': string; 'link': string; - 'nextAlbum': string; - 'overlayTitle': string; + 'nextAiring': string; 'title': string; } export const cssExports: CssExports; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js new file mode 100644 index 000000000..43c7ca22b --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js @@ -0,0 +1,271 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ArtistBanner from 'Artist/ArtistBanner'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import { icons } from 'Helpers/Props'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import translate from 'Utilities/String/translate'; +import ArtistIndexBannerInfo from './ArtistIndexBannerInfo'; +import styles from './ArtistIndexBanner.css'; + +class ArtistIndexBanner extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + }; + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + }; + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + }; + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + }; + + // + // Render + + render() { + const { + id, + artistName, + monitored, + status, + foreignArtistId, + nextAiring, + statistics, + images, + bannerWidth, + bannerHeight, + detailedProgressBar, + showTitle, + showMonitored, + showQualityProfile, + showSearchAction, + qualityProfile, + showRelativeDates, + shortDateFormat, + timeFormat, + isRefreshingArtist, + isSearchingArtist, + onRefreshArtistPress, + onSearchPress, + ...otherProps + } = this.props; + + const { + albumCount, + sizeOnDisk, + trackCount, + trackFileCount, + totalTrackCount + } = statistics; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const link = `/artist/${foreignArtistId}`; + + const elementStyle = { + width: `${bannerWidth}px`, + height: `${bannerHeight}px` + }; + + return ( +
+
+
+ + + { + status === 'ended' && +
+ } + + + + +
+ + + + { + showTitle && +
+ {artistName} +
+ } + + { + showMonitored && +
+ {monitored ? 'Monitored' : 'Unmonitored'} +
+ } + + { + showQualityProfile && +
+ {qualityProfile.name} +
+ } + { + nextAiring && +
+ { + getRelativeDate( + nextAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ } + + + + + + +
+
+ ); + } +} + +ArtistIndexBanner.propTypes = { + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + foreignArtistId: PropTypes.string.isRequired, + nextAiring: PropTypes.string, + statistics: PropTypes.object.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + bannerWidth: PropTypes.number.isRequired, + bannerHeight: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showTitle: PropTypes.bool.isRequired, + showMonitored: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + qualityProfile: PropTypes.object.isRequired, + showSearchAction: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + isSearchingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +ArtistIndexBanner.defaultProps = { + statistics: { + albumCount: 0, + trackCount: 0, + trackFileCount: 0, + totalTrackCount: 0 + } +}; + +export default ArtistIndexBanner; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.tsx b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.tsx deleted file mode 100644 index f6a03c521..000000000 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Statistics } from 'Artist/Artist'; -import ArtistBanner from 'Artist/ArtistBanner'; -import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; -import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; -import ArtistIndexBannerInfo from 'Artist/Index/Banners/ArtistIndexBannerInfo'; -import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector'; -import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; -import ArtistIndexPosterSelect from 'Artist/Index/Select/ArtistIndexPosterSelect'; -import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import { icons } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import getRelativeDate from 'Utilities/Date/getRelativeDate'; -import translate from 'Utilities/String/translate'; -import selectBannerOptions from './selectBannerOptions'; -import styles from './ArtistIndexBanner.css'; - -interface ArtistIndexBannerProps { - artistId: number; - sortKey: string; - isSelectMode: boolean; - bannerWidth: number; - bannerHeight: number; -} - -function ArtistIndexBanner(props: ArtistIndexBannerProps) { - const { artistId, sortKey, isSelectMode, bannerWidth, bannerHeight } = props; - - const { - artist, - qualityProfile, - metadataProfile, - isRefreshingArtist, - isSearchingArtist, - } = useSelector(createArtistIndexItemSelector(props.artistId)); - - const { - detailedProgressBar, - showTitle, - showMonitored, - showQualityProfile, - showNextAlbum, - showSearchAction, - } = useSelector(selectBannerOptions); - - const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } = - useSelector(createUISettingsSelector()); - - const { - artistName, - artistType, - monitored, - status, - path, - foreignArtistId, - nextAlbum, - added, - statistics = {} as Statistics, - images, - tags, - } = artist; - - const { - albumCount = 0, - trackCount = 0, - trackFileCount = 0, - totalTrackCount = 0, - sizeOnDisk = 0, - } = statistics; - - const dispatch = useDispatch(); - const [hasBannerError, setHasBannerError] = useState(false); - const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false); - const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false); - - const onRefreshPress = useCallback(() => { - dispatch( - executeCommand({ - name: REFRESH_ARTIST, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onSearchPress = useCallback(() => { - dispatch( - executeCommand({ - name: ARTIST_SEARCH, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onBannerLoadError = useCallback(() => { - setHasBannerError(true); - }, [setHasBannerError]); - - const onBannerLoad = useCallback(() => { - setHasBannerError(false); - }, [setHasBannerError]); - - const onEditArtistPress = useCallback(() => { - setIsEditArtistModalOpen(true); - }, [setIsEditArtistModalOpen]); - - const onEditArtistModalClose = useCallback(() => { - setIsEditArtistModalOpen(false); - }, [setIsEditArtistModalOpen]); - - const onDeleteArtistPress = useCallback(() => { - setIsEditArtistModalOpen(false); - setIsDeleteArtistModalOpen(true); - }, [setIsDeleteArtistModalOpen]); - - const onDeleteArtistModalClose = useCallback(() => { - setIsDeleteArtistModalOpen(false); - }, [setIsDeleteArtistModalOpen]); - - const link = `/artist/${foreignArtistId}`; - - const elementStyle = { - width: `${bannerWidth}px`, - height: `${bannerHeight}px`, - }; - - return ( -
-
- {isSelectMode ? : null} - - - - {status === 'ended' ? ( -
- ) : null} - - - - - {hasBannerError ? ( -
{artistName}
- ) : null} - -
- - - - {showTitle ? ( -
- {artistName} -
- ) : null} - - {showMonitored ? ( -
- {monitored ? translate('Monitored') : translate('Unmonitored')} -
- ) : null} - - {showQualityProfile && !!qualityProfile?.name ? ( -
- {qualityProfile.name} -
- ) : null} - - {showNextAlbum && !!nextAlbum?.releaseDate ? ( -
- {getRelativeDate( - nextAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - )} -
- ) : null} - - - - - - -
- ); -} - -export default ArtistIndexBanner; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js new file mode 100644 index 000000000..f641de0e1 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import styles from './ArtistIndexBannerInfo.css'; + +function ArtistIndexBannerInfo(props) { + const { + qualityProfile, + showQualityProfile, + previousAiring, + added, + albumCount, + path, + sizeOnDisk, + sortKey, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + if (sortKey === 'qualityProfileId' && !showQualityProfile) { + return ( +
+ {qualityProfile.name} +
+ ); + } + + if (sortKey === 'previousAiring' && previousAiring) { + return ( +
+ { + getRelativeDate( + previousAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ ); + } + + if (sortKey === 'added' && added) { + const addedDate = getRelativeDate( + added, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: false + } + ); + + return ( +
+ {`Added ${addedDate}`} +
+ ); + } + + if (sortKey === 'albumCount') { + let albums = '1 album'; + + if (albumCount === 0) { + albums = 'No albums'; + } else if (albumCount > 1) { + albums = `${albumCount} albums`; + } + + return ( +
+ {albums} +
+ ); + } + + if (sortKey === 'path') { + return ( +
+ {path} +
+ ); + } + + if (sortKey === 'sizeOnDisk') { + return ( +
+ {formatBytes(sizeOnDisk)} +
+ ); + } + + return null; +} + +ArtistIndexBannerInfo.propTypes = { + qualityProfile: PropTypes.object.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + previousAiring: PropTypes.string, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexBannerInfo; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.tsx b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.tsx deleted file mode 100644 index a93b0bafc..000000000 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React from 'react'; -import Album from 'Album/Album'; -import TagListConnector from 'Components/TagListConnector'; -import MetadataProfile from 'typings/MetadataProfile'; -import QualityProfile from 'typings/QualityProfile'; -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 styles from './ArtistIndexBannerInfo.css'; - -interface ArtistIndexBannerInfoProps { - artistType?: string; - showQualityProfile: boolean; - qualityProfile?: QualityProfile; - metadataProfile?: MetadataProfile; - showNextAlbum: boolean; - nextAlbum?: Album; - lastAlbum?: Album; - added?: string; - albumCount: number; - path: string; - sizeOnDisk?: number; - tags?: number[]; - sortKey: string; - showRelativeDates: boolean; - shortDateFormat: string; - longDateFormat: string; - timeFormat: string; -} - -function ArtistIndexBannerInfo(props: ArtistIndexBannerInfoProps) { - const { - artistType, - qualityProfile, - metadataProfile, - showQualityProfile, - showNextAlbum, - nextAlbum, - lastAlbum, - added, - albumCount, - path, - sizeOnDisk, - tags, - sortKey, - showRelativeDates, - shortDateFormat, - longDateFormat, - timeFormat, - } = props; - - if (sortKey === 'artistType' && artistType) { - return ( -
- {artistType} -
- ); - } - - if ( - sortKey === 'qualityProfileId' && - !showQualityProfile && - !!qualityProfile?.name - ) { - return ( -
- {qualityProfile.name} -
- ); - } - - if (sortKey === 'metadataProfileId' && !!metadataProfile?.name) { - return ( -
- {metadataProfile.name} -
- ); - } - - if (sortKey === 'nextAlbum' && !showNextAlbum && !!nextAlbum?.releaseDate) { - return ( -
- {getRelativeDate( - nextAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - )} -
- ); - } - - if (sortKey === 'lastAlbum' && !!lastAlbum?.releaseDate) { - return ( -
- {getRelativeDate( - lastAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - )} -
- ); - } - - if (sortKey === 'added' && added) { - const addedDate = getRelativeDate( - added, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: false, - } - ); - - return ( -
- {translate('Added')}: {addedDate} -
- ); - } - - if (sortKey === 'albumCount') { - let albums = translate('OneAlbum'); - - if (albumCount === 0) { - albums = translate('NoAlbums'); - } else if (albumCount > 1) { - albums = translate('CountAlbums', { albumCount }); - } - - return
{albums}
; - } - - if (sortKey === 'path') { - return ( -
- {path} -
- ); - } - - if (sortKey === 'sizeOnDisk') { - return ( -
- {formatBytes(sizeOnDisk)} -
- ); - } - - if (sortKey === 'tags' && tags) { - return ( -
- -
- ); - } - - return null; -} - -export default ArtistIndexBannerInfo; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js new file mode 100644 index 000000000..be3cdb502 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js @@ -0,0 +1,327 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Grid, WindowScroller } from 'react-virtualized'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import Measure from 'Components/Measure'; +import dimensions from 'Styles/Variables/dimensions'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; +import ArtistIndexBanner from './ArtistIndexBanner'; +import styles from './ArtistIndexBanners.css'; + +// container dimensions +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +const additionalColumnCount = { + small: 3, + medium: 2, + large: 1 +}; + +function calculateColumnWidth(width, bannerSize, isSmallScreen) { + const maxiumColumnWidth = isSmallScreen ? 344 : 364; + const columns = Math.floor(width / maxiumColumnWidth); + const remainder = width % maxiumColumnWidth; + + if (remainder === 0 && bannerSize === 'large') { + return maxiumColumnWidth; + } + + return Math.floor(width / (columns + additionalColumnCount[bannerSize])); +} + +function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions) { + const { + detailedProgressBar, + showTitle, + showMonitored, + showQualityProfile + } = bannerOptions; + + const nextAiringHeight = 19; + + const heights = [ + bannerHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + nextAiringHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + if (showTitle) { + heights.push(19); + } + + if (showMonitored) { + heights.push(19); + } + + if (showQualityProfile) { + heights.push(19); + } + + switch (sortKey) { + case 'seasons': + case 'previousAiring': + case 'added': + case 'path': + case 'sizeOnDisk': + heights.push(19); + break; + case 'qualityProfileId': + if (!showQualityProfile) { + heights.push(19); + } + break; + default: + // No need to add a height of 0 + } + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculateHeight(bannerWidth) { + return Math.ceil((88/476) * bannerWidth); +} + +class ArtistIndexBanners extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnWidth: 364, + columnCount: 1, + bannerWidth: 476, + bannerHeight: 88, + rowHeight: calculateRowHeight(88, null, props.isSmallScreen, {}), + scrollRestored: false + }; + + this._isInitialized = false; + this._grid = null; + } + + componentDidUpdate(prevProps, prevState) { + const { + items, + sortKey, + bannerOptions, + jumpToCharacter, + scrollTop + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight, + scrollRestored + } = this.state; + + if (prevProps.sortKey !== sortKey || + prevProps.bannerOptions !== bannerOptions) { + this.calculateGrid(); + } + + if (this._grid && + (prevState.width !== width || + prevState.columnWidth !== columnWidth || + prevState.columnCount !== columnCount || + prevState.rowHeight !== rowHeight || + hasDifferentItemsOrOrder(prevProps.items, items))) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells + this._grid.recomputeGridSize(); + } + + if (this._grid && scrollTop !== 0 && !scrollRestored) { + this.setState({ scrollRestored: true }); + this._grid.scrollToPosition({ scrollTop }); + } + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + const index = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (this._grid && index != null) { + const row = Math.floor(index / columnCount); + + this._grid.scrollToCell({ + rowIndex: row, + columnIndex: 0 + }); + } + } + + } + + // + // Control + + setGridRef = (ref) => { + this._grid = ref; + }; + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + bannerOptions + } = this.props; + + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + const columnWidth = calculateColumnWidth(width, bannerOptions.size, isSmallScreen); + const columnCount = Math.max(Math.floor(width / columnWidth), 1); + const bannerWidth = columnWidth - padding; + const bannerHeight = calculateHeight(bannerWidth); + const rowHeight = calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions); + + this.setState({ + width, + columnWidth, + columnCount, + bannerWidth, + bannerHeight, + rowHeight + }); + }; + + cellRenderer = ({ key, rowIndex, columnIndex, style }) => { + const { + items, + sortKey, + bannerOptions, + showRelativeDates, + shortDateFormat, + timeFormat + } = this.props; + + const { + bannerWidth, + bannerHeight, + columnCount + } = this.state; + + const { + detailedProgressBar, + showTitle, + showMonitored, + showQualityProfile + } = bannerOptions; + + const artist = items[rowIndex * columnCount + columnIndex]; + + if (!artist) { + return null; + } + + return ( +
+ +
+ ); + }; + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + }; + + // + // Render + + render() { + const { + items, + isSmallScreen, + scroller + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; + + const rowCount = Math.ceil(items.length / columnCount); + + return ( + + + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return
; + } + + return ( + + ); + } + } + + + ); + } +} + +ArtistIndexBanners.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + bannerOptions: PropTypes.object.isRequired, + jumpToCharacter: PropTypes.string, + scrollTop: PropTypes.number.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexBanners; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.tsx b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.tsx deleted file mode 100644 index 3582da097..000000000 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { throttle } from 'lodash'; -import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner'; -import useMeasure from 'Helpers/Hooks/useMeasure'; -import SortDirection from 'Helpers/Props/SortDirection'; -import dimensions from 'Styles/Variables/dimensions'; -import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; - -const bodyPadding = parseInt(dimensions.pageContentBodyPadding); -const bodyPaddingSmallScreen = parseInt( - dimensions.pageContentBodyPaddingSmallScreen -); -const columnPadding = parseInt(dimensions.artistIndexColumnPadding); -const columnPaddingSmallScreen = parseInt( - dimensions.artistIndexColumnPaddingSmallScreen -); -const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); -const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); - -const ADDITIONAL_COLUMN_COUNT: Record = { - small: 3, - medium: 2, - large: 1, -}; - -interface CellItemData { - layout: { - columnCount: number; - padding: number; - bannerWidth: number; - bannerHeight: number; - }; - items: Artist[]; - sortKey: string; - isSelectMode: boolean; -} - -interface ArtistIndexBannersProps { - items: Artist[]; - sortKey: string; - sortDirection?: SortDirection; - jumpToCharacter?: string; - scrollTop?: number; - scrollerRef: RefObject; - isSelectMode: boolean; - isSmallScreen: boolean; -} - -const artistIndexSelector = createSelector( - (state: AppState) => state.artistIndex.bannerOptions, - (bannerOptions) => { - return { - bannerOptions, - }; - } -); - -const Cell: React.FC> = ({ - columnIndex, - rowIndex, - style, - data, -}) => { - const { layout, items, sortKey, isSelectMode } = data; - const { columnCount, padding, bannerWidth, bannerHeight } = layout; - const index = rowIndex * columnCount + columnIndex; - - if (index >= items.length) { - return null; - } - - const artist = items[index]; - - return ( -
- -
- ); -}; - -function getWindowScrollTopPosition() { - return document.documentElement.scrollTop || document.body.scrollTop || 0; -} - -export default function ArtistIndexBanners(props: ArtistIndexBannersProps) { - const { - scrollerRef, - items, - sortKey, - jumpToCharacter, - isSelectMode, - isSmallScreen, - } = props; - - const { bannerOptions } = useSelector(artistIndexSelector); - const ref = useRef(null); - const [measureRef, bounds] = useMeasure(); - const [size, setSize] = useState({ width: 0, height: 0 }); - - const columnWidth = useMemo(() => { - const { width } = size; - const maximumColumnWidth = isSmallScreen ? 344 : 364; - const columns = Math.floor(width / maximumColumnWidth); - const remainder = width % maximumColumnWidth; - return remainder === 0 - ? maximumColumnWidth - : Math.floor( - width / (columns + ADDITIONAL_COLUMN_COUNT[bannerOptions.size]) - ); - }, [isSmallScreen, bannerOptions, size]); - - const columnCount = useMemo( - () => Math.max(Math.floor(size.width / columnWidth), 1), - [size, columnWidth] - ); - const padding = props.isSmallScreen - ? columnPaddingSmallScreen - : columnPadding; - const bannerWidth = columnWidth - padding * 2; - const bannerHeight = Math.ceil((88 / 476) * bannerWidth); - - const rowHeight = useMemo(() => { - const { - detailedProgressBar, - showTitle, - showMonitored, - showQualityProfile, - showNextAlbum, - } = bannerOptions; - - const nextAiringHeight = 19; - - const heights = [ - bannerHeight, - detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, - nextAiringHeight, - isSmallScreen ? columnPaddingSmallScreen : columnPadding, - ]; - - if (showTitle) { - heights.push(19); - } - - if (showMonitored) { - heights.push(19); - } - - if (showQualityProfile) { - heights.push(19); - } - - if (showNextAlbum) { - heights.push(19); - } - - switch (sortKey) { - case 'artistType': - case 'metadataProfileId': - case 'lastAlbum': - case 'added': - case 'albumCount': - case 'path': - case 'sizeOnDisk': - case 'tags': - heights.push(19); - break; - case 'qualityProfileId': - if (!showQualityProfile) { - heights.push(19); - } - break; - case 'nextAlbum': - if (!showNextAlbum) { - heights.push(19); - } - break; - default: - // No need to add a height of 0 - } - - return heights.reduce((acc, height) => acc + height, 0); - }, [isSmallScreen, bannerOptions, sortKey, bannerHeight]); - - useEffect(() => { - const current = scrollerRef.current; - - if (isSmallScreen) { - const padding = bodyPaddingSmallScreen - 5; - - setSize({ - width: window.innerWidth - padding * 2, - height: window.innerHeight, - }); - - return; - } - - if (current) { - const width = current.clientWidth; - const padding = bodyPadding - 5; - - setSize({ - width: width - padding * 2, - height: window.innerHeight, - }); - } - }, [isSmallScreen, scrollerRef, bounds]); - - useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; - - const handleScroll = throttle(() => { - const { offsetTop = 0 } = currentScrollerRef; - const scrollTop = - (isSmallScreen - ? getWindowScrollTopPosition() - : currentScrollerRef.scrollTop) - offsetTop; - - ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); - }, 10); - - currentScrollListener.addEventListener('scroll', handleScroll); - - return () => { - handleScroll.cancel(); - - if (currentScrollListener) { - currentScrollListener.removeEventListener('scroll', handleScroll); - } - }; - }, [isSmallScreen, ref, scrollerRef]); - - useEffect(() => { - if (jumpToCharacter) { - const index = getIndexOfFirstCharacter(items, jumpToCharacter); - - if (index != null) { - const rowIndex = Math.floor(index / columnCount); - - const scrollTop = rowIndex * rowHeight + padding; - - ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); - scrollerRef.current?.scrollTo(0, scrollTop); - } - } - }, [ - jumpToCharacter, - rowHeight, - columnCount, - padding, - items, - scrollerRef, - ref, - ]); - - return ( -
- - ref={ref} - style={{ - width: '100%', - height: '100%', - overflow: 'none', - }} - width={size.width} - height={size.height} - columnCount={columnCount} - columnWidth={columnWidth} - rowCount={Math.ceil(items.length / columnCount)} - rowHeight={rowHeight} - itemData={{ - layout: { - columnCount, - padding, - bannerWidth, - bannerHeight, - }, - items, - sortKey, - isSelectMode, - }} - > - {Cell} - -
- ); -} diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js new file mode 100644 index 000000000..1cf68ba2b --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import ArtistIndexBanners from './ArtistIndexBanners'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex.bannerOptions, + createUISettingsSelector(), + createDimensionsSelector(), + (bannerOptions, uiSettings, dimensions) => { + return { + bannerOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(ArtistIndexBanners); diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js new file mode 100644 index 000000000..34c8abfcf --- /dev/null +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ArtistIndexBannerOptionsModalContentConnector from './ArtistIndexBannerOptionsModalContentConnector'; + +function ArtistIndexBannerOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +ArtistIndexBannerOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexBannerOptionsModal; diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.tsx b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.tsx deleted file mode 100644 index 156e06079..000000000 --- a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ArtistIndexBannerOptionsModalContent from './ArtistIndexBannerOptionsModalContent'; - -interface ArtistIndexBannerOptionsModalProps { - isOpen: boolean; - onModalClose(...args: unknown[]): unknown; -} - -function ArtistIndexBannerOptionsModal({ - isOpen, - onModalClose, -}: ArtistIndexBannerOptionsModalProps) { - return ( - - - - ); -} - -export default ArtistIndexBannerOptionsModal; diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js new file mode 100644 index 000000000..8951a7b3d --- /dev/null +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js @@ -0,0 +1,226 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +const bannerSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class ArtistIndexBannerOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showTitle: props.showTitle, + showMonitored: props.showMonitored, + showQualityProfile: props.showQualityProfile, + showSearchAction: props.showSearchAction + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showTitle, + showMonitored, + showQualityProfile, + showSearchAction + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showTitle !== prevProps.showTitle) { + state.showTitle = showTitle; + } + + if (showMonitored !== prevProps.showMonitored) { + state.showMonitored = showMonitored; + } + + if (showQualityProfile !== prevProps.showQualityProfile) { + state.showQualityProfile = showQualityProfile; + } + + if (showSearchAction !== prevProps.showSearchAction) { + state.showSearchAction = showSearchAction; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangeBannerOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangeBannerOption({ [name]: value }); + }); + }; + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + detailedProgressBar, + size, + showTitle, + showMonitored, + showQualityProfile, + showSearchAction + } = this.state; + + return ( + + + Options + + + +
+ + + {translate('Size')} + + + + + + + + {translate('DetailedProgressBar')} + + + + + + + + {translate('ShowName')} + + + + + + + + {translate('ShowMonitored')} + + + + + + + + {translate('ShowQualityProfile')} + + + + + + + + {translate('ShowSearch')} + + + + +
+
+ + + + +
+ ); + } +} + +ArtistIndexBannerOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showTitle: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showSearchAction: PropTypes.bool.isRequired, + onChangeBannerOption: PropTypes.func.isRequired, + showMonitored: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexBannerOptionsModalContent; diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.tsx b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.tsx deleted file mode 100644 index f889ea450..000000000 --- a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import selectBannerOptions from 'Artist/Index/Banners/selectBannerOptions'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import { setArtistBannerOption } from 'Store/Actions/artistIndexActions'; -import translate from 'Utilities/String/translate'; - -const bannerSizeOptions = [ - { - key: 'small', - get value() { - return translate('Small'); - }, - }, - { - key: 'medium', - get value() { - return translate('Medium'); - }, - }, - { - key: 'large', - get value() { - return translate('Large'); - }, - }, -]; - -interface ArtistIndexBannerOptionsModalContentProps { - onModalClose(...args: unknown[]): unknown; -} - -function ArtistIndexBannerOptionsModalContent( - props: ArtistIndexBannerOptionsModalContentProps -) { - const { onModalClose } = props; - - const bannerOptions = useSelector(selectBannerOptions); - - const { - detailedProgressBar, - size, - showTitle, - showMonitored, - showQualityProfile, - showNextAlbum, - showSearchAction, - } = bannerOptions; - - const dispatch = useDispatch(); - - const onBannerOptionChange = useCallback( - ({ name, value }: { name: string; value: unknown }) => { - dispatch(setArtistBannerOption({ [name]: value })); - }, - [dispatch] - ); - - return ( - - {translate('BannerOptions')} - - -
- - {translate('BannerSize')} - - - - - - {translate('DetailedProgressBar')} - - - - - - {translate('ShowName')} - - - - - - {translate('ShowMonitored')} - - - - - - {translate('ShowQualityProfile')} - - - - - - {translate('ShowNextAlbum')} - - - - - - {translate('ShowSearch')} - - - -
-
- - - - -
- ); -} - -export default ArtistIndexBannerOptionsModalContent; diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js new file mode 100644 index 000000000..884edd05d --- /dev/null +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistBannerOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexBannerOptionsModalContent from './ArtistIndexBannerOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex, + (artistIndex) => { + return artistIndex.bannerOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangeBannerOption(payload) { + dispatch(setArtistBannerOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexBannerOptionsModalContent); diff --git a/frontend/src/Artist/Index/Banners/selectBannerOptions.ts b/frontend/src/Artist/Index/Banners/selectBannerOptions.ts deleted file mode 100644 index 529c15e06..000000000 --- a/frontend/src/Artist/Index/Banners/selectBannerOptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -const selectBannerOptions = createSelector( - (state: AppState) => state.artistIndex.bannerOptions, - (bannerOptions) => bannerOptions -); - -export default selectBannerOptions; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js new file mode 100644 index 000000000..d146fdf7d --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ArtistIndexFilterModalConnector from 'Artist/Index/ArtistIndexFilterModalConnector'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import { align } from 'Helpers/Props'; + +function ArtistIndexFilterMenu(props) { + const { + selectedFilterKey, + filters, + customFilters, + isDisabled, + onFilterSelect + } = props; + + return ( + + ); +} + +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 +}; + +export default ArtistIndexFilterMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.tsx b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.tsx deleted file mode 100644 index 91ebbef2d..000000000 --- a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.tsx +++ /dev/null @@ -1,41 +0,0 @@ -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) { - const { - selectedFilterKey, - filters, - customFilters, - isDisabled, - onFilterSelect, - } = props; - - return ( - - ); -} - -ArtistIndexFilterMenu.defaultProps = { - showCustomFilters: false, -}; - -export default ArtistIndexFilterMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js similarity index 74% rename from frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx rename to frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js index 1b72d0f4c..967b34d49 100644 --- a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.tsx +++ b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js @@ -1,23 +1,23 @@ +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) { - const { sortKey, sortDirection, isDisabled, onSortSelect } = props; +function ArtistIndexSortMenu(props) { + const { + sortKey, + sortDirection, + isDisabled, + onSortSelect + } = props; return ( - + - {translate('MonitoredStatus')} + Monitored/Status - {translate('Name')} + Name - {translate('Type')} + Type - {translate('QualityProfile')} + Quality Profile - {translate('MetadataProfile')} + Metadata Profile - {translate('NextAlbum')} + Next Album - {translate('LastAlbum')} + Last Album - {translate('Added')} + Added - {translate('Albums')} + Albums - {translate('Tracks')} + Tracks - {translate('TrackCount')} + Track Count - {translate('Path')} + Path - {translate('SizeOnDisk')} + Size on Disk - {translate('Tags')} + Tags ); } +ArtistIndexSortMenu.propTypes = { + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + isDisabled: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired +}; + export default ArtistIndexSortMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js new file mode 100644 index 000000000..246409a7b --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import MenuContent from 'Components/Menu/MenuContent'; +import ViewMenu from 'Components/Menu/ViewMenu'; +import ViewMenuItem from 'Components/Menu/ViewMenuItem'; +import { align } from 'Helpers/Props'; + +function ArtistIndexViewMenu(props) { + const { + view, + isDisabled, + onViewSelect + } = props; + + return ( + + + + Table + + + + Posters + + + + Banners + + + + Overview + + + + ); +} + +ArtistIndexViewMenu.propTypes = { + view: PropTypes.string.isRequired, + isDisabled: PropTypes.bool.isRequired, + onViewSelect: PropTypes.func.isRequired +}; + +export default ArtistIndexViewMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.tsx b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.tsx deleted file mode 100644 index bb88d9149..000000000 --- a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import MenuContent from 'Components/Menu/MenuContent'; -import ViewMenu from 'Components/Menu/ViewMenu'; -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) { - const { view, isDisabled, onViewSelect } = props; - - return ( - - - - {translate('Table')} - - - - {translate('Posters')} - - - - {translate('Banners')} - - - - {translate('Overview')} - - - - ); -} - -export default ArtistIndexViewMenu; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css index 1f482a2d6..3b1888228 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css @@ -1,5 +1,13 @@ $hoverScale: 1.05; +.container { + &:hover { + .content { + background-color: var(--tableRowHoverBackgroundColor); + } + } +} + .content { display: flex; flex-grow: 1; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css.d.ts b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css.d.ts index de94277cc..76a72536a 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css.d.ts +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'actions': string; + 'container': string; 'content': string; 'details': string; 'ended': string; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js new file mode 100644 index 000000000..1baac838f --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js @@ -0,0 +1,283 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TextTruncate from 'react-text-truncate'; +import ArtistPoster from 'Artist/ArtistPoster'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import { icons } from 'Helpers/Props'; +import dimensions from 'Styles/Variables/dimensions'; +import fonts from 'Styles/Variables/fonts'; +import translate from 'Utilities/String/translate'; +import ArtistIndexOverviewInfo from './ArtistIndexOverviewInfo'; +import styles from './ArtistIndexOverview.css'; + +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const defaultFontSize = parseInt(fonts.defaultFontSize); +const lineHeight = parseFloat(fonts.lineHeight); + +// Hardcoded height beased on line-height of 32 + bottom margin of 10. +// Less side-effecty than using react-measure. +const titleRowHeight = 42; + +function getContentHeight(rowHeight, isSmallScreen) { + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + + return rowHeight - padding; +} + +class ArtistIndexOverview extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + }; + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + }; + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + }; + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + }; + + // + // Render + + render() { + const { + id, + artistName, + overview, + monitored, + status, + foreignArtistId, + nextAiring, + statistics, + images, + posterWidth, + posterHeight, + qualityProfile, + overviewOptions, + showSearchAction, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + rowHeight, + isSmallScreen, + isRefreshingArtist, + isSearchingArtist, + onRefreshArtistPress, + onSearchPress, + ...otherProps + } = this.props; + + const { + albumCount, + sizeOnDisk, + trackCount, + trackFileCount, + totalTrackCount + } = statistics; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const link = `/artist/${foreignArtistId}`; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px` + }; + + const contentHeight = getContentHeight(rowHeight, isSmallScreen); + const overviewHeight = contentHeight - titleRowHeight; + + return ( +
+
+
+
+ { + status === 'ended' && +
+ } + + + + +
+ + +
+ +
+
+ + {artistName} + + +
+ + + { + showSearchAction && + + } + + +
+
+ +
+ + + + + + +
+
+
+ + + + +
+ ); + } +} + +ArtistIndexOverview.propTypes = { + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + overview: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + foreignArtistId: PropTypes.string.isRequired, + nextAiring: PropTypes.string, + statistics: PropTypes.object.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + qualityProfile: PropTypes.object.isRequired, + overviewOptions: PropTypes.object.isRequired, + showSearchAction: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + isSearchingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +ArtistIndexOverview.defaultProps = { + statistics: { + albumCount: 0, + trackCount: 0, + trackFileCount: 0, + totalTrackCount: 0 + } +}; + +export default ArtistIndexOverview; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.tsx deleted file mode 100644 index ebef28264..000000000 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import TextTruncate from 'react-text-truncate'; -import { Statistics } from 'Artist/Artist'; -import ArtistPoster from 'Artist/ArtistPoster'; -import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; -import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; -import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; -import ArtistIndexPosterSelect from 'Artist/Index/Select/ArtistIndexPosterSelect'; -import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import { icons } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import dimensions from 'Styles/Variables/dimensions'; -import fonts from 'Styles/Variables/fonts'; -import translate from 'Utilities/String/translate'; -import createArtistIndexItemSelector from '../createArtistIndexItemSelector'; -import ArtistIndexOverviewInfo from './ArtistIndexOverviewInfo'; -import selectOverviewOptions from './selectOverviewOptions'; -import styles from './ArtistIndexOverview.css'; - -const columnPadding = parseInt(dimensions.artistIndexColumnPadding); -const columnPaddingSmallScreen = parseInt( - dimensions.artistIndexColumnPaddingSmallScreen -); -const defaultFontSize = parseInt(fonts.defaultFontSize); -const lineHeight = parseFloat(fonts.lineHeight); - -// Hardcoded height based on line-height of 32 + bottom margin of 10. -// Less side-effecty than using react-measure. -const TITLE_HEIGHT = 42; - -interface ArtistIndexOverviewProps { - artistId: number; - sortKey: string; - posterWidth: number; - posterHeight: number; - rowHeight: number; - isSelectMode: boolean; - isSmallScreen: boolean; -} - -function ArtistIndexOverview(props: ArtistIndexOverviewProps) { - const { - artistId, - sortKey, - posterWidth, - posterHeight, - rowHeight, - isSelectMode, - isSmallScreen, - } = props; - - const { artist, qualityProfile, isRefreshingArtist, isSearchingArtist } = - useSelector(createArtistIndexItemSelector(props.artistId)); - - const overviewOptions = useSelector(selectOverviewOptions); - - const { - artistName, - monitored, - status, - path, - foreignArtistId, - nextAlbum, - lastAlbum, - added, - overview, - statistics = {} as Statistics, - images, - } = artist; - - const { - albumCount = 0, - sizeOnDisk = 0, - trackCount = 0, - trackFileCount = 0, - totalTrackCount = 0, - } = statistics; - - const dispatch = useDispatch(); - const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false); - const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false); - - const onRefreshPress = useCallback(() => { - dispatch( - executeCommand({ - name: REFRESH_ARTIST, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onSearchPress = useCallback(() => { - dispatch( - executeCommand({ - name: ARTIST_SEARCH, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onEditArtistPress = useCallback(() => { - setIsEditArtistModalOpen(true); - }, [setIsEditArtistModalOpen]); - - const onEditArtistModalClose = useCallback(() => { - setIsEditArtistModalOpen(false); - }, [setIsEditArtistModalOpen]); - - const onDeleteArtistPress = useCallback(() => { - setIsEditArtistModalOpen(false); - setIsDeleteArtistModalOpen(true); - }, [setIsDeleteArtistModalOpen]); - - const onDeleteArtistModalClose = useCallback(() => { - setIsDeleteArtistModalOpen(false); - }, [setIsDeleteArtistModalOpen]); - - const link = `/artist/${foreignArtistId}`; - - const elementStyle = { - width: `${posterWidth}px`, - height: `${posterHeight}px`, - }; - - const contentHeight = useMemo(() => { - const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; - - return rowHeight - padding; - }, [rowHeight, isSmallScreen]); - - const overviewHeight = contentHeight - TITLE_HEIGHT; - - return ( -
-
-
-
- {isSelectMode ? ( - - ) : null} - - {status === 'ended' && ( -
- )} - - - - -
- - -
- -
-
- - {artistName} - - -
- - - {overviewOptions.showSearchAction ? ( - - ) : null} - - -
-
- -
- - - - - -
-
-
- - - - -
- ); -} - -export default ArtistIndexOverview; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js new file mode 100644 index 000000000..f7cda7916 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js @@ -0,0 +1,249 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import dimensions from 'Styles/Variables/dimensions'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow'; +import styles from './ArtistIndexOverviewInfo.css'; + +const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight); + +const rows = [ + { + name: 'monitored', + showProp: 'showMonitored', + valueProp: 'monitored' + }, + { + name: 'qualityProfileId', + showProp: 'showQualityProfile', + valueProp: 'qualityProfileId' + }, + { + name: 'lastAlbum', + showProp: 'showLastAlbum', + valueProp: 'lastAlbum' + }, + { + name: 'added', + showProp: 'showAdded', + valueProp: 'added' + }, + { + name: 'albumCount', + showProp: 'showAlbumCount', + valueProp: 'albumCount' + }, + { + name: 'path', + showProp: 'showPath', + valueProp: 'path' + }, + { + name: 'sizeOnDisk', + showProp: 'showSizeOnDisk', + valueProp: 'sizeOnDisk' + } +]; + +function isVisible(row, props) { + const { + name, + showProp, + valueProp + } = row; + + if (props[valueProp] == null) { + return false; + } + + return props[showProp] || props.sortKey === name; +} + +function getInfoRowProps(row, props) { + const { name } = row; + + if (name === 'monitored') { + const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored'; + + return { + title: monitoredText, + iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED, + label: monitoredText + }; + } + + if (name === 'qualityProfileId') { + return { + title: 'Quality Profile', + iconName: icons.PROFILE, + label: props.qualityProfile.name + }; + } + + if (name === 'lastAlbum') { + const { + lastAlbum, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + return { + title: `Last Album: ${lastAlbum.title}`, + iconName: icons.CALENDAR, + label: getRelativeDate( + lastAlbum.releaseDate, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + }; + } + + if (name === 'added') { + const { + added, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat + } = props; + + return { + title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`, + iconName: icons.ADD, + label: getRelativeDate( + added, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + }; + } + + if (name === 'albumCount') { + const { albumCount } = props; + let albums = '1 album'; + + if (albumCount === 0) { + albums = 'No albums'; + } else if (albumCount > 1) { + albums = `${albumCount} albums`; + } + + return { + title: 'Album Count', + iconName: icons.CIRCLE, + label: albums + }; + } + + if (name === 'path') { + return { + title: 'Path', + iconName: icons.FOLDER, + label: props.path + }; + } + + if (name === 'sizeOnDisk') { + return { + title: 'Size on Disk', + iconName: icons.DRIVE, + label: formatBytes(props.sizeOnDisk) + }; + } +} + +function ArtistIndexOverviewInfo(props) { + const { + height, + nextAiring, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat + } = props; + + let shownRows = 1; + + const maxRows = Math.floor(height / (infoRowHeight + 4)); + + return ( +
+ { + !!nextAiring && + + } + + { + rows.map((row) => { + if (!isVisible(row, props)) { + return null; + } + + if (shownRows >= maxRows) { + return null; + } + + shownRows++; + + const infoRowProps = getInfoRowProps(row, props); + + return ( + + ); + }) + } +
+ ); +} + +ArtistIndexOverviewInfo.propTypes = { + height: PropTypes.number.isRequired, + showMonitored: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + showAdded: PropTypes.bool.isRequired, + showAlbumCount: PropTypes.bool.isRequired, + showPath: PropTypes.bool.isRequired, + showSizeOnDisk: PropTypes.bool.isRequired, + monitored: PropTypes.bool.isRequired, + nextAiring: PropTypes.string, + qualityProfile: PropTypes.object.isRequired, + lastAlbum: PropTypes.object, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexOverviewInfo; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.tsx deleted file mode 100644 index c95d34b84..000000000 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.tsx +++ /dev/null @@ -1,260 +0,0 @@ -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 = [ - { - name: 'monitored', - showProp: 'showMonitored', - valueProp: 'monitored', - }, - { - name: 'qualityProfileId', - showProp: 'showQualityProfile', - valueProp: 'qualityProfile', - }, - { - name: 'lastAlbum', - showProp: 'showLastAlbum', - valueProp: 'lastAlbum', - }, - { - name: 'added', - showProp: 'showAdded', - valueProp: 'added', - }, - { - name: 'albumCount', - showProp: 'showAlbumCount', - valueProp: 'albumCount', - }, - { - name: 'path', - showProp: 'showPath', - valueProp: 'path', - }, - { - name: 'sizeOnDisk', - showProp: 'showSizeOnDisk', - valueProp: 'sizeOnDisk', - }, -]; - -function getInfoRowProps( - row: RowProps, - props: ArtistIndexOverviewInfoProps, - uiSettings: UiSettings -): RowInfoProps | null { - const { name } = row; - - if (name === 'monitored') { - const monitoredText = props.monitored - ? translate('Monitored') - : translate('Unmonitored'); - - return { - title: monitoredText, - iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED, - label: monitoredText, - }; - } - - if (name === 'qualityProfileId' && !!props.qualityProfile?.name) { - return { - title: translate('QualityProfile'), - iconName: icons.PROFILE, - label: props.qualityProfile.name, - }; - } - - if (name === 'lastAlbum' && !!props.lastAlbum?.title) { - const lastAlbum = props.lastAlbum; - const { showRelativeDates, shortDateFormat, timeFormat } = uiSettings; - - return { - title: `Last Album: ${lastAlbum.title}`, - iconName: icons.CALENDAR, - label: - getRelativeDate( - lastAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - ) ?? '', - }; - } - - if (name === 'added') { - const added = props.added; - const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } = - uiSettings; - - return { - title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`, - iconName: icons.ADD, - label: - getRelativeDate(added, shortDateFormat, showRelativeDates, { - timeFormat, - timeForToday: true, - }) ?? '', - }; - } - - if (name === 'albumCount') { - const { albumCount } = props; - let albums = '1 album'; - - if (albumCount === 0) { - albums = 'No albums'; - } else if (albumCount > 1) { - albums = `${albumCount} albums`; - } - - return { - title: translate('AlbumCount'), - iconName: icons.CIRCLE, - label: albums, - }; - } - - if (name === 'path') { - return { - title: translate('Path'), - iconName: icons.FOLDER, - label: props.path, - }; - } - - if (name === 'sizeOnDisk') { - return { - title: translate('SizeOnDisk'), - iconName: icons.DRIVE, - label: formatBytes(props.sizeOnDisk), - }; - } - - return null; -} - -function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) { - const { height, nextAlbum } = props; - - const uiSettings = useSelector(createUISettingsSelector()); - - const { shortDateFormat, showRelativeDates, longDateFormat, timeFormat } = - uiSettings; - - let shownRows = 1; - const maxRows = Math.floor(height / (infoRowHeight + 4)); - - const rowInfo = useMemo(() => { - return rows.map((row) => { - 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 { - ...row, - isVisible, - }; - }); - }, [props]); - - return ( -
- {!!nextAlbum?.releaseDate && ( - - )} - - {rowInfo.map((row) => { - if (!row.isVisible) { - return null; - } - - if (shownRows >= maxRows) { - return null; - } - - shownRows++; - - const infoRowProps = getInfoRowProps(row, props, uiSettings); - - if (infoRowProps == null) { - return null; - } - - return ; - })} -
- ); -} - -export default ArtistIndexOverviewInfo; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.js new file mode 100644 index 000000000..b04029b88 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import styles from './ArtistIndexOverviewInfoRow.css'; + +function ArtistIndexOverviewInfoRow(props) { + const { + title, + iconName, + label + } = props; + + return ( +
+ + + {label} +
+ ); +} + +ArtistIndexOverviewInfoRow.propTypes = { + title: PropTypes.string, + iconName: PropTypes.object.isRequired, + label: PropTypes.string.isRequired +}; + +export default ArtistIndexOverviewInfoRow; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx deleted file mode 100644 index 5d9b4a069..000000000 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfoRow.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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; -} - -function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) { - const { title, iconName, label } = props; - - return ( -
- - - {label} -
- ); -} - -export default ArtistIndexOverviewInfoRow; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js new file mode 100644 index 000000000..101092170 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js @@ -0,0 +1,275 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Grid, WindowScroller } from 'react-virtualized'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import Measure from 'Components/Measure'; +import dimensions from 'Styles/Variables/dimensions'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; +import ArtistIndexOverview from './ArtistIndexOverview'; +import styles from './ArtistIndexOverviews.css'; + +// Poster container dimensions +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +function calculatePosterWidth(posterSize, isSmallScreen) { + const maxiumPosterWidth = isSmallScreen ? 192 : 202; + + if (posterSize === 'large') { + return maxiumPosterWidth; + } + + if (posterSize === 'medium') { + return Math.floor(maxiumPosterWidth * 0.75); + } + + return Math.floor(maxiumPosterWidth * 0.5); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) { + const { + detailedProgressBar + } = overviewOptions; + + const heights = [ + posterHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculatePosterHeight(posterWidth) { + return posterWidth; +} + +class ArtistIndexOverviews extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnCount: 1, + posterWidth: 238, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}), + scrollRestored: false + }; + + this._grid = null; + } + + componentDidUpdate(prevProps, prevState) { + const { + items, + sortKey, + overviewOptions, + jumpToCharacter, + scrollTop, + isSmallScreen + } = this.props; + + const { + width, + rowHeight, + scrollRestored + } = this.state; + + if (prevProps.sortKey !== sortKey || + prevProps.overviewOptions !== overviewOptions) { + this.calculateGrid(this.state.width, isSmallScreen); + } + + if ( + this._grid && + (prevState.width !== width || + prevState.rowHeight !== rowHeight || + hasDifferentItemsOrOrder(prevProps.items, items) || + prevProps.overviewOptions !== overviewOptions + ) + ) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells + this._grid.recomputeGridSize(); + } + + if (this._grid && scrollTop !== 0 && !scrollRestored) { + this.setState({ scrollRestored: true }); + this._grid.scrollToPosition({ scrollTop }); + } + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + const index = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (this._grid && index != null) { + + this._grid.scrollToCell({ + rowIndex: index, + columnIndex: 0 + }); + } + } + } + + // + // Control + + setGridRef = (ref) => { + this._grid = ref; + }; + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + overviewOptions + } = this.props; + + const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen); + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions); + + this.setState({ + width, + posterWidth, + posterHeight, + rowHeight + }); + }; + + cellRenderer = ({ key, rowIndex, style }) => { + const { + items, + sortKey, + overviewOptions, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + isSmallScreen + } = this.props; + + const { + posterWidth, + posterHeight, + rowHeight + } = this.state; + + const artist = items[rowIndex]; + + if (!artist) { + return null; + } + + return ( +
+ +
+ ); + }; + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + }; + + // + // Render + + render() { + const { + items, + isSmallScreen, + scroller + } = this.props; + + const { + width, + rowHeight + } = this.state; + + return ( + + + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return
; + } + + return ( +
+ +
+ ); + } + } + + + ); + } +} + +ArtistIndexOverviews.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + overviewOptions: PropTypes.object.isRequired, + scrollTop: PropTypes.number.isRequired, + jumpToCharacter: PropTypes.string, + scroller: PropTypes.instanceOf(Element).isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexOverviews; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx deleted file mode 100644 index 11285c1b3..000000000 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { throttle } from 'lodash'; -import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; -import Artist from 'Artist/Artist'; -import useMeasure from 'Helpers/Hooks/useMeasure'; -import dimensions from 'Styles/Variables/dimensions'; -import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; -import ArtistIndexOverview from './ArtistIndexOverview'; -import selectOverviewOptions from './selectOverviewOptions'; - -// Poster container dimensions -const columnPadding = parseInt(dimensions.artistIndexColumnPadding); -const columnPaddingSmallScreen = parseInt( - dimensions.artistIndexColumnPaddingSmallScreen -); -const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); -const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); -const bodyPadding = parseInt(dimensions.pageContentBodyPadding); -const bodyPaddingSmallScreen = parseInt( - dimensions.pageContentBodyPaddingSmallScreen -); - -interface RowItemData { - items: Artist[]; - sortKey: string; - posterWidth: number; - posterHeight: number; - rowHeight: number; - isSelectMode: boolean; - isSmallScreen: boolean; -} - -interface ArtistIndexOverviewsProps { - items: Artist[]; - sortKey: string; - sortDirection?: string; - jumpToCharacter?: string; - scrollTop?: number; - scrollerRef: RefObject; - isSelectMode: boolean; - isSmallScreen: boolean; -} - -const Row: React.FC> = ({ - index, - style, - data, -}) => { - const { items, ...otherData } = data; - - if (index >= items.length) { - return null; - } - - const artist = items[index]; - - return ( -
- -
- ); -}; - -function getWindowScrollTopPosition() { - return document.documentElement.scrollTop || document.body.scrollTop || 0; -} - -function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) { - const { - items, - sortKey, - jumpToCharacter, - scrollerRef, - isSelectMode, - isSmallScreen, - } = props; - - const { size: posterSize, detailedProgressBar } = useSelector( - selectOverviewOptions - ); - const listRef = useRef(null); - const [measureRef, bounds] = useMeasure(); - const [size, setSize] = useState({ width: 0, height: 0 }); - - const posterWidth = useMemo(() => { - const maxiumPosterWidth = isSmallScreen ? 192 : 202; - - if (posterSize === 'large') { - return maxiumPosterWidth; - } - - if (posterSize === 'medium') { - return Math.floor(maxiumPosterWidth * 0.75); - } - - return Math.floor(maxiumPosterWidth * 0.5); - }, [posterSize, isSmallScreen]); - - const posterHeight = useMemo(() => { - return posterWidth; - }, [posterWidth]); - - const rowHeight = useMemo(() => { - const heights = [ - posterHeight, - detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, - isSmallScreen ? columnPaddingSmallScreen : columnPadding, - ]; - - return heights.reduce((acc, height) => acc + height, 0); - }, [detailedProgressBar, posterHeight, isSmallScreen]); - - useEffect(() => { - const current = scrollerRef.current as HTMLElement; - - if (isSmallScreen) { - setSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - - return; - } - - if (current) { - const width = current.clientWidth; - const padding = - (isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5; - - setSize({ - width: width - padding * 2, - height: window.innerHeight, - }); - } - }, [isSmallScreen, scrollerRef, bounds]); - - useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; - - const handleScroll = throttle(() => { - const { offsetTop = 0 } = currentScrollerRef; - const scrollTop = - (isSmallScreen - ? getWindowScrollTopPosition() - : currentScrollerRef.scrollTop) - offsetTop; - - listRef.current?.scrollTo(scrollTop); - }, 10); - - currentScrollListener.addEventListener('scroll', handleScroll); - - return () => { - handleScroll.cancel(); - - if (currentScrollListener) { - currentScrollListener.removeEventListener('scroll', handleScroll); - } - }; - }, [isSmallScreen, listRef, scrollerRef]); - - useEffect(() => { - if (jumpToCharacter) { - const index = getIndexOfFirstCharacter(items, jumpToCharacter); - - if (index != null) { - let scrollTop = index * rowHeight; - - // If the offset is zero go to the top, otherwise offset - // by the approximate size of the header + padding (37 + 20). - if (scrollTop > 0) { - const offset = 57; - - scrollTop += offset; - } - - listRef.current?.scrollTo(scrollTop); - scrollerRef.current?.scrollTo(0, scrollTop); - } - } - }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); - - return ( -
- - ref={listRef} - style={{ - width: '100%', - height: '100%', - overflow: 'none', - }} - width={size.width} - height={size.height} - itemCount={items.length} - itemSize={rowHeight} - itemData={{ - items, - sortKey, - posterWidth, - posterHeight, - rowHeight, - isSelectMode, - isSmallScreen, - }} - > - {Row} - -
- ); -} - -export default ArtistIndexOverviews; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js new file mode 100644 index 000000000..030e8999b --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import ArtistIndexOverviews from './ArtistIndexOverviews'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex.overviewOptions, + createUISettingsSelector(), + createDimensionsSelector(), + (overviewOptions, uiSettings, dimensions) => { + return { + overviewOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(ArtistIndexOverviews); diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.js new file mode 100644 index 000000000..9ca575185 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ArtistIndexOverviewOptionsModalContentConnector from './ArtistIndexOverviewOptionsModalContentConnector'; + +function ArtistIndexOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +ArtistIndexOverviewOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexOverviewOptionsModal; diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.tsx b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.tsx deleted file mode 100644 index bc999cee4..000000000 --- a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ArtistIndexOverviewOptionsModalContent from './ArtistIndexOverviewOptionsModalContent'; - -interface ArtistIndexOverviewOptionsModalProps { - isOpen: boolean; - onModalClose(...args: unknown[]): void; -} - -function ArtistIndexOverviewOptionsModal({ - isOpen, - onModalClose, - ...otherProps -}: ArtistIndexOverviewOptionsModalProps) { - return ( - - - - ); -} - -export default ArtistIndexOverviewOptionsModal; diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js new file mode 100644 index 000000000..226f46a1b --- /dev/null +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js @@ -0,0 +1,308 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +const posterSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class ArtistIndexOverviewOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showMonitored: props.showMonitored, + showQualityProfile: props.showQualityProfile, + showLastAlbum: props.showLastAlbum, + showAdded: props.showAdded, + showAlbumCount: props.showAlbumCount, + showPath: props.showPath, + showSizeOnDisk: props.showSizeOnDisk, + showSearchAction: props.showSearchAction + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showMonitored, + showQualityProfile, + showLastAlbum, + showAdded, + showAlbumCount, + showPath, + showSizeOnDisk, + showSearchAction + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showMonitored !== prevProps.showMonitored) { + state.showMonitored = showMonitored; + } + + if (showQualityProfile !== prevProps.showQualityProfile) { + state.showQualityProfile = showQualityProfile; + } + + if (showLastAlbum !== prevProps.showLastAlbum) { + state.showLastAlbum = showLastAlbum; + } + + if (showAdded !== prevProps.showAdded) { + state.showAdded = showAdded; + } + + if (showAlbumCount !== prevProps.showAlbumCount) { + state.showAlbumCount = showAlbumCount; + } + + if (showPath !== prevProps.showPath) { + state.showPath = showPath; + } + + if (showSizeOnDisk !== prevProps.showSizeOnDisk) { + state.showSizeOnDisk = showSizeOnDisk; + } + + if (showSearchAction !== prevProps.showSearchAction) { + state.showSearchAction = showSearchAction; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangeOverviewOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangeOverviewOption({ [name]: value }); + }); + }; + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + detailedProgressBar, + size, + showMonitored, + showQualityProfile, + showLastAlbum, + showAdded, + showAlbumCount, + showPath, + showSizeOnDisk, + showSearchAction + } = this.state; + + return ( + + + Overview Options + + + +
+ + + {translate('PosterSize')} + + + + + + + + {translate('DetailedProgressBar')} + + + + + + + + {translate('ShowMonitored')} + + + + + + + + + {translate('ShowQualityProfile')} + + + + + + + + {translate('ShowLastAlbum')} + + + + + + + + {translate('ShowDateAdded')} + + + + + + + + {translate('ShowAlbumCount')} + + + + + + + + {translate('ShowPath')} + + + + + + + + {translate('ShowSizeOnDisk')} + + + + + + + + {translate('ShowSearch')} + + + + +
+
+ + + + +
+ ); + } +} + +ArtistIndexOverviewOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showMonitored: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + showLastAlbum: PropTypes.bool.isRequired, + showAdded: PropTypes.bool.isRequired, + showAlbumCount: PropTypes.bool.isRequired, + showPath: PropTypes.bool.isRequired, + showSizeOnDisk: PropTypes.bool.isRequired, + showSearchAction: PropTypes.bool.isRequired, + onChangeOverviewOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexOverviewOptionsModalContent; diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx deleted file mode 100644 index 4ab9391e3..000000000 --- a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import { setArtistOverviewOption } from 'Store/Actions/artistIndexActions'; -import translate from 'Utilities/String/translate'; -import selectOverviewOptions from '../selectOverviewOptions'; - -const posterSizeOptions = [ - { - key: 'small', - get value() { - return translate('Small'); - }, - }, - { - key: 'medium', - get value() { - return translate('Medium'); - }, - }, - { - key: 'large', - get value() { - return translate('Large'); - }, - }, -]; - -interface ArtistIndexOverviewOptionsModalContentProps { - onModalClose(...args: unknown[]): void; -} - -function ArtistIndexOverviewOptionsModalContent( - props: ArtistIndexOverviewOptionsModalContentProps -) { - const { onModalClose } = props; - - const { - detailedProgressBar, - size, - showMonitored, - showQualityProfile, - showLastAlbum, - showAdded, - showAlbumCount, - showPath, - showSizeOnDisk, - showSearchAction, - } = useSelector(selectOverviewOptions); - - const dispatch = useDispatch(); - - const onOverviewOptionChange = useCallback( - ({ name, value }: { name: string; value: unknown }) => { - dispatch(setArtistOverviewOption({ [name]: value })); - }, - [dispatch] - ); - - return ( - - {translate('OverviewOptions')} - - -
- - {translate('PosterSize')} - - - - - - {translate('DetailedProgressBar')} - - - - - - {translate('ShowMonitored')} - - - - - - {translate('ShowQualityProfile')} - - - - - - {translate('ShowLastAlbum')} - - - - - - {translate('ShowDateAdded')} - - - - - - {translate('ShowAlbumCount')} - - - - - - {translate('ShowPath')} - - - - - - {translate('ShowSizeOnDisk')} - - - - - - {translate('ShowSearch')} - - - -
-
- - - - -
- ); -} - -export default ArtistIndexOverviewOptionsModalContent; diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContentConnector.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContentConnector.js new file mode 100644 index 000000000..70c30dba6 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistOverviewOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexOverviewOptionsModalContent from './ArtistIndexOverviewOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex, + (artistIndex) => { + return artistIndex.overviewOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangeOverviewOption(payload) { + dispatch(setArtistOverviewOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexOverviewOptionsModalContent); diff --git a/frontend/src/Artist/Index/Overview/selectOverviewOptions.ts b/frontend/src/Artist/Index/Overview/selectOverviewOptions.ts deleted file mode 100644 index 5875163c8..000000000 --- a/frontend/src/Artist/Index/Overview/selectOverviewOptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -const selectOverviewOptions = createSelector( - (state: AppState) => state.artistIndex.overviewOptions, - (overviewOptions) => overviewOptions -); - -export default selectOverviewOptions; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js new file mode 100644 index 000000000..455736ff1 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js @@ -0,0 +1,305 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ArtistPoster from 'Artist/ArtistPoster'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import { icons } from 'Helpers/Props'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import translate from 'Utilities/String/translate'; +import ArtistIndexPosterInfo from './ArtistIndexPosterInfo'; +import styles from './ArtistIndexPoster.css'; + +class ArtistIndexPoster extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPosterError: false, + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + }; + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + }; + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + }; + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + }; + + onPosterLoad = () => { + if (this.state.hasPosterError) { + this.setState({ hasPosterError: false }); + } + }; + + onPosterLoadError = () => { + if (!this.state.hasPosterError) { + this.setState({ hasPosterError: true }); + } + }; + + // + // Render + + render() { + const { + id, + artistName, + monitored, + foreignArtistId, + status, + nextAlbum, + lastAlbum, + statistics, + images, + posterWidth, + posterHeight, + detailedProgressBar, + showTitle, + showMonitored, + showQualityProfile, + qualityProfile, + showNextAlbum, + showSearchAction, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + isRefreshingArtist, + isSearchingArtist, + onRefreshArtistPress, + onSearchPress, + ...otherProps + } = this.props; + + const { + albumCount, + sizeOnDisk, + trackCount, + trackFileCount, + totalTrackCount + } = statistics; + + const { + hasPosterError, + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const link = `/artist/${foreignArtistId}`; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px` + }; + + return ( +
+
+
+ + + { + status === 'ended' && +
+ } + + + + + { + hasPosterError && +
+ {artistName} +
+ } + + +
+ + + + { + showTitle && +
+ {artistName} +
+ } + + { + showMonitored && +
+ {monitored ? 'Monitored' : 'Unmonitored'} +
+ } + + { + showQualityProfile && +
+ {qualityProfile.name} +
+ } + + { + showNextAlbum && !!nextAlbum?.releaseDate && +
+ { + getRelativeDate( + nextAlbum.releaseDate, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ } + + + + + +
+
+ ); + } +} + +ArtistIndexPoster.propTypes = { + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + foreignArtistId: PropTypes.string.isRequired, + nextAlbum: PropTypes.object, + lastAlbum: PropTypes.object, + statistics: PropTypes.object.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showTitle: PropTypes.bool.isRequired, + showMonitored: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + qualityProfile: PropTypes.object.isRequired, + showNextAlbum: PropTypes.bool.isRequired, + showSearchAction: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + isSearchingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +ArtistIndexPoster.defaultProps = { + statistics: { + albumCount: 0, + trackCount: 0, + trackFileCount: 0, + totalTrackCount: 0 + } +}; + +export default ArtistIndexPoster; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx deleted file mode 100644 index 67c37c00d..000000000 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Statistics } from 'Artist/Artist'; -import ArtistPoster from 'Artist/ArtistPoster'; -import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; -import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; -import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector'; -import ArtistIndexPosterInfo from 'Artist/Index/Posters/ArtistIndexPosterInfo'; -import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; -import ArtistIndexPosterSelect from 'Artist/Index/Select/ArtistIndexPosterSelect'; -import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import { icons } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import getRelativeDate from 'Utilities/Date/getRelativeDate'; -import translate from 'Utilities/String/translate'; -import selectPosterOptions from './selectPosterOptions'; -import styles from './ArtistIndexPoster.css'; - -interface ArtistIndexPosterProps { - artistId: number; - sortKey: string; - isSelectMode: boolean; - posterWidth: number; - posterHeight: number; -} - -function ArtistIndexPoster(props: ArtistIndexPosterProps) { - const { artistId, sortKey, isSelectMode, posterWidth, posterHeight } = props; - - const { - artist, - qualityProfile, - metadataProfile, - isRefreshingArtist, - isSearchingArtist, - } = useSelector(createArtistIndexItemSelector(props.artistId)); - - const { - detailedProgressBar, - showTitle, - showMonitored, - showQualityProfile, - showNextAlbum, - showSearchAction, - } = useSelector(selectPosterOptions); - - const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } = - useSelector(createUISettingsSelector()); - - const { - artistName, - artistType, - monitored, - status, - path, - foreignArtistId, - nextAlbum, - added, - statistics = {} as Statistics, - images, - tags, - } = artist; - - const { - albumCount = 0, - trackCount = 0, - trackFileCount = 0, - totalTrackCount = 0, - sizeOnDisk = 0, - } = statistics; - - const dispatch = useDispatch(); - const [hasPosterError, setHasPosterError] = useState(false); - const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false); - const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false); - - const onRefreshPress = useCallback(() => { - dispatch( - executeCommand({ - name: REFRESH_ARTIST, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onSearchPress = useCallback(() => { - dispatch( - executeCommand({ - name: ARTIST_SEARCH, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onPosterLoadError = useCallback(() => { - setHasPosterError(true); - }, [setHasPosterError]); - - const onPosterLoad = useCallback(() => { - setHasPosterError(false); - }, [setHasPosterError]); - - const onEditArtistPress = useCallback(() => { - setIsEditArtistModalOpen(true); - }, [setIsEditArtistModalOpen]); - - const onEditArtistModalClose = useCallback(() => { - setIsEditArtistModalOpen(false); - }, [setIsEditArtistModalOpen]); - - const onDeleteArtistPress = useCallback(() => { - setIsEditArtistModalOpen(false); - setIsDeleteArtistModalOpen(true); - }, [setIsDeleteArtistModalOpen]); - - const onDeleteArtistModalClose = useCallback(() => { - setIsDeleteArtistModalOpen(false); - }, [setIsDeleteArtistModalOpen]); - - const link = `/artist/${foreignArtistId}`; - - const elementStyle = { - width: `${posterWidth}px`, - height: `${posterHeight}px`, - }; - - return ( -
-
- {isSelectMode ? : null} - - - - {status === 'ended' ? ( -
- ) : null} - - - - - {hasPosterError ? ( -
{artistName}
- ) : null} - -
- - - - {showTitle ? ( -
- {artistName} -
- ) : null} - - {showMonitored ? ( -
- {monitored ? translate('Monitored') : translate('Unmonitored')} -
- ) : null} - - {showQualityProfile && !!qualityProfile?.name ? ( -
- {qualityProfile.name} -
- ) : null} - - {showNextAlbum && !!nextAlbum?.releaseDate ? ( -
- {getRelativeDate( - nextAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - )} -
- ) : null} - - - - - - -
- ); -} - -export default ArtistIndexPoster; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.tsx b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js similarity index 63% rename from frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.tsx rename to frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js index 0d4ff9135..20a34bffd 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.tsx +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js @@ -1,39 +1,16 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import Album from 'Album/Album'; import TagListConnector from 'Components/TagListConnector'; -import MetadataProfile from 'typings/MetadataProfile'; -import QualityProfile from 'typings/QualityProfile'; 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 styles from './ArtistIndexPosterInfo.css'; -interface ArtistIndexPosterInfoProps { - artistType?: string; - showQualityProfile: boolean; - qualityProfile?: QualityProfile; - metadataProfile?: MetadataProfile; - showNextAlbum: boolean; - nextAlbum?: Album; - lastAlbum?: Album; - added?: string; - albumCount: number; - path: string; - sizeOnDisk?: number; - tags?: number[]; - sortKey: string; - showRelativeDates: boolean; - shortDateFormat: string; - longDateFormat: string; - timeFormat: string; -} - -function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { +function ArtistIndexPosterInfo(props) { const { artistType, qualityProfile, - metadataProfile, showQualityProfile, showNextAlbum, nextAlbum, @@ -47,7 +24,7 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { showRelativeDates, shortDateFormat, longDateFormat, - timeFormat, + timeFormat } = props; if (sortKey === 'artistType' && artistType) { @@ -58,11 +35,7 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { ); } - if ( - sortKey === 'qualityProfileId' && - !showQualityProfile && - !!qualityProfile?.name - ) { + if (sortKey === 'qualityProfileId' && !showQualityProfile) { return (
{qualityProfile.name} @@ -70,14 +43,6 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { ); } - if (sortKey === 'metadataProfileId' && !!metadataProfile?.name) { - return ( -
- {metadataProfile.name} -
- ); - } - if (sortKey === 'nextAlbum' && !showNextAlbum && !!nextAlbum?.releaseDate) { return (
- {getRelativeDate( - nextAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - )} + { + getRelativeDate( + nextAlbum.releaseDate, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + }
); } @@ -111,15 +78,17 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { timeFormat )}`} > - {getRelativeDate( - lastAlbum.releaseDate, - shortDateFormat, - showRelativeDates, - { - timeFormat, - timeForToday: true, - } - )} + { + getRelativeDate( + lastAlbum.releaseDate, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + }
); } @@ -131,7 +100,7 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { showRelativeDates, { timeFormat, - timeForToday: false, + timeForToday: false } ); @@ -154,7 +123,11 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { albums = translate('CountAlbums', { albumCount }); } - return
{albums}
; + return ( +
+ {albums} +
+ ); } if (sortKey === 'path') { @@ -173,10 +146,12 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { ); } - if (sortKey === 'tags' && tags) { + if (sortKey === 'tags') { return (
- +
); } @@ -184,4 +159,23 @@ function ArtistIndexPosterInfo(props: ArtistIndexPosterInfoProps) { return null; } +ArtistIndexPosterInfo.propTypes = { + artistType: PropTypes.string, + qualityProfile: PropTypes.object.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + showNextAlbum: PropTypes.bool.isRequired, + nextAlbum: PropTypes.object, + lastAlbum: PropTypes.object, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + export default ArtistIndexPosterInfo; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js new file mode 100644 index 000000000..69df97c7c --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js @@ -0,0 +1,351 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Grid, WindowScroller } from 'react-virtualized'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import Measure from 'Components/Measure'; +import dimensions from 'Styles/Variables/dimensions'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; +import ArtistIndexPoster from './ArtistIndexPoster'; +import styles from './ArtistIndexPosters.css'; + +// Poster container dimensions +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +const additionalColumnCount = { + small: 3, + medium: 2, + large: 1 +}; + +function calculateColumnWidth(width, posterSize, isSmallScreen) { + const maxiumColumnWidth = isSmallScreen ? 172 : 182; + const columns = Math.floor(width / maxiumColumnWidth); + const remainder = width % maxiumColumnWidth; + + if (remainder === 0 && posterSize === 'large') { + return maxiumColumnWidth; + } + + return Math.floor(width / (columns + additionalColumnCount[posterSize])); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) { + const { + detailedProgressBar, + showTitle, + showMonitored, + showQualityProfile, + showNextAlbum + } = posterOptions; + + const nextAiringHeight = 19; + + const heights = [ + posterHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + nextAiringHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + if (showTitle) { + heights.push(19); + } + + if (showMonitored) { + heights.push(19); + } + + if (showQualityProfile) { + heights.push(19); + } + + if (showNextAlbum) { + heights.push(19); + } + + switch (sortKey) { + case 'artistType': + case 'lastAlbum': + case 'seasons': + case 'added': + case 'albumCount': + case 'path': + case 'sizeOnDisk': + case 'tags': + heights.push(19); + break; + case 'qualityProfileId': + if (!showQualityProfile) { + heights.push(19); + } + break; + case 'nextAlbum': + if (!showNextAlbum) { + heights.push(19); + } + break; + default: + // No need to add a height of 0 + } + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculatePosterHeight(posterWidth) { + return Math.ceil(posterWidth); +} + +class ArtistIndexPosters extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnWidth: 182, + columnCount: 1, + posterWidth: 238, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}), + scrollRestored: false + }; + + this._isInitialized = false; + this._grid = null; + this._padding = props.isSmallScreen ? columnPaddingSmallScreen : columnPadding; + } + + componentDidUpdate(prevProps, prevState) { + const { + items, + sortKey, + posterOptions, + jumpToCharacter, + scrollTop, + isSmallScreen + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight, + scrollRestored + } = this.state; + + if (prevProps.sortKey !== sortKey || + prevProps.posterOptions !== posterOptions) { + this.calculateGrid(width, isSmallScreen); + } + + if (this._grid && + (prevState.width !== width || + prevState.columnWidth !== columnWidth || + prevState.columnCount !== columnCount || + prevState.rowHeight !== rowHeight || + hasDifferentItemsOrOrder(prevProps.items, items))) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells + this._grid.recomputeGridSize(); + } + + if (this._grid && scrollTop !== 0 && !scrollRestored) { + this.setState({ scrollRestored: true }); + this._grid.scrollToPosition({ scrollTop }); + } + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + const index = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (this._grid && index != null) { + const row = Math.floor(index / columnCount); + + this._grid.scrollToCell({ + rowIndex: row, + columnIndex: 0 + }); + } + } + } + + // + // Control + + setGridRef = (ref) => { + this._grid = ref; + }; + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + posterOptions + } = this.props; + + const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen); + const columnCount = Math.max(Math.floor(width / columnWidth), 1); + const posterWidth = columnWidth - this._padding * 2; + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions); + + this.setState({ + width, + columnWidth, + columnCount, + posterWidth, + posterHeight, + rowHeight + }); + }; + + cellRenderer = ({ key, rowIndex, columnIndex, style }) => { + const { + items, + sortKey, + posterOptions, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat + } = this.props; + + const { + posterWidth, + posterHeight, + columnCount + } = this.state; + + const { + detailedProgressBar, + showTitle, + showMonitored, + showQualityProfile, + showNextAlbum + } = posterOptions; + + const artist = items[rowIndex * columnCount + columnIndex]; + + if (!artist) { + return null; + } + + return ( +
+ +
+ ); + }; + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + }; + + // + // Render + + render() { + const { + scroller, + items, + isSmallScreen + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; + + const rowCount = Math.ceil(items.length / columnCount); + + return ( + + + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return
; + } + + return ( +
+ +
+ ); + } + } + + + ); + } +} + +ArtistIndexPosters.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + posterOptions: PropTypes.object.isRequired, + jumpToCharacter: PropTypes.string, + scrollTop: PropTypes.number.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired +}; + +export default ArtistIndexPosters; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx deleted file mode 100644 index c478ac1ae..000000000 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { throttle } from 'lodash'; -import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster'; -import useMeasure from 'Helpers/Hooks/useMeasure'; -import SortDirection from 'Helpers/Props/SortDirection'; -import dimensions from 'Styles/Variables/dimensions'; -import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; - -const bodyPadding = parseInt(dimensions.pageContentBodyPadding); -const bodyPaddingSmallScreen = parseInt( - dimensions.pageContentBodyPaddingSmallScreen -); -const columnPadding = parseInt(dimensions.artistIndexColumnPadding); -const columnPaddingSmallScreen = parseInt( - dimensions.artistIndexColumnPaddingSmallScreen -); -const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); -const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); - -const ADDITIONAL_COLUMN_COUNT: Record = { - small: 3, - medium: 2, - large: 1, -}; - -interface CellItemData { - layout: { - columnCount: number; - padding: number; - posterWidth: number; - posterHeight: number; - }; - items: Artist[]; - sortKey: string; - isSelectMode: boolean; -} - -interface ArtistIndexPostersProps { - items: Artist[]; - sortKey: string; - sortDirection?: SortDirection; - jumpToCharacter?: string; - scrollTop?: number; - scrollerRef: RefObject; - isSelectMode: boolean; - isSmallScreen: boolean; -} - -const artistIndexSelector = createSelector( - (state: AppState) => state.artistIndex.posterOptions, - (posterOptions) => { - return { - posterOptions, - }; - } -); - -const Cell: React.FC> = ({ - columnIndex, - rowIndex, - style, - data, -}) => { - const { layout, items, sortKey, isSelectMode } = data; - const { columnCount, padding, posterWidth, posterHeight } = layout; - const index = rowIndex * columnCount + columnIndex; - - if (index >= items.length) { - return null; - } - - const artist = items[index]; - - return ( -
- -
- ); -}; - -function getWindowScrollTopPosition() { - return document.documentElement.scrollTop || document.body.scrollTop || 0; -} - -export default function ArtistIndexPosters(props: ArtistIndexPostersProps) { - const { - scrollerRef, - items, - sortKey, - jumpToCharacter, - isSelectMode, - isSmallScreen, - } = props; - - const { posterOptions } = useSelector(artistIndexSelector); - const ref = useRef(null); - const [measureRef, bounds] = useMeasure(); - const [size, setSize] = useState({ width: 0, height: 0 }); - - const columnWidth = useMemo(() => { - const { width } = size; - const maximumColumnWidth = isSmallScreen ? 172 : 182; - const columns = Math.floor(width / maximumColumnWidth); - const remainder = width % maximumColumnWidth; - return remainder === 0 - ? maximumColumnWidth - : Math.floor( - width / (columns + ADDITIONAL_COLUMN_COUNT[posterOptions.size]) - ); - }, [isSmallScreen, posterOptions, size]); - - const columnCount = useMemo( - () => Math.max(Math.floor(size.width / columnWidth), 1), - [size, columnWidth] - ); - const padding = props.isSmallScreen - ? columnPaddingSmallScreen - : columnPadding; - const posterWidth = columnWidth - padding * 2; - const posterHeight = Math.ceil(posterWidth); - - const rowHeight = useMemo(() => { - const { - detailedProgressBar, - showTitle, - showMonitored, - showQualityProfile, - showNextAlbum, - } = posterOptions; - - const nextAiringHeight = 19; - - const heights = [ - posterHeight, - detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, - nextAiringHeight, - isSmallScreen ? columnPaddingSmallScreen : columnPadding, - ]; - - if (showTitle) { - heights.push(19); - } - - if (showMonitored) { - heights.push(19); - } - - if (showQualityProfile) { - heights.push(19); - } - - if (showNextAlbum) { - heights.push(19); - } - - switch (sortKey) { - case 'artistType': - case 'metadataProfileId': - case 'lastAlbum': - case 'added': - case 'albumCount': - case 'path': - case 'sizeOnDisk': - case 'tags': - heights.push(19); - break; - case 'qualityProfileId': - if (!showQualityProfile) { - heights.push(19); - } - break; - case 'nextAlbum': - if (!showNextAlbum) { - heights.push(19); - } - break; - default: - // No need to add a height of 0 - } - - return heights.reduce((acc, height) => acc + height, 0); - }, [isSmallScreen, posterOptions, sortKey, posterHeight]); - - useEffect(() => { - const current = scrollerRef.current; - - if (isSmallScreen) { - const padding = bodyPaddingSmallScreen - 5; - const width = window.innerWidth - padding * 2; - const height = window.innerHeight; - - if (width !== size.width || height !== size.height) { - setSize({ - width, - height, - }); - } - - return; - } - - if (current) { - const width = current.clientWidth; - const padding = bodyPadding - 5; - const finalWidth = width - padding * 2; - - if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) { - return; - } - - setSize({ - width: finalWidth, - height: window.innerHeight, - }); - } - }, [isSmallScreen, size, scrollerRef, bounds]); - - useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; - - const handleScroll = throttle(() => { - const { offsetTop = 0 } = currentScrollerRef; - const scrollTop = - (isSmallScreen - ? getWindowScrollTopPosition() - : currentScrollerRef.scrollTop) - offsetTop; - - ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); - }, 10); - - currentScrollListener.addEventListener('scroll', handleScroll); - - return () => { - handleScroll.cancel(); - - if (currentScrollListener) { - currentScrollListener.removeEventListener('scroll', handleScroll); - } - }; - }, [isSmallScreen, ref, scrollerRef]); - - useEffect(() => { - if (jumpToCharacter) { - const index = getIndexOfFirstCharacter(items, jumpToCharacter); - - if (index != null) { - const rowIndex = Math.floor(index / columnCount); - - const scrollTop = rowIndex * rowHeight + padding; - - ref.current?.scrollTo({ scrollLeft: 0, scrollTop }); - scrollerRef.current?.scrollTo(0, scrollTop); - } - } - }, [ - jumpToCharacter, - rowHeight, - columnCount, - padding, - items, - scrollerRef, - ref, - ]); - - return ( -
- - ref={ref} - style={{ - width: '100%', - height: '100%', - overflow: 'none', - }} - width={size.width} - height={size.height} - columnCount={columnCount} - columnWidth={columnWidth} - rowCount={Math.ceil(items.length / columnCount)} - rowHeight={rowHeight} - itemData={{ - layout: { - columnCount, - padding, - posterWidth, - posterHeight, - }, - items, - sortKey, - isSelectMode, - }} - > - {Cell} - -
- ); -} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js new file mode 100644 index 000000000..bff8bef81 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import ArtistIndexPosters from './ArtistIndexPosters'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex.posterOptions, + createUISettingsSelector(), + createDimensionsSelector(), + (posterOptions, uiSettings, dimensions) => { + return { + posterOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(ArtistIndexPosters); diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js new file mode 100644 index 000000000..e1b0a257a --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ArtistIndexPosterOptionsModalContentConnector from './ArtistIndexPosterOptionsModalContentConnector'; + +function ArtistIndexPosterOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +ArtistIndexPosterOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexPosterOptionsModal; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.tsx b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.tsx deleted file mode 100644 index 69368807a..000000000 --- a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ArtistIndexPosterOptionsModalContent from './ArtistIndexPosterOptionsModalContent'; - -interface ArtistIndexPosterOptionsModalProps { - isOpen: boolean; - onModalClose(...args: unknown[]): unknown; -} - -function ArtistIndexPosterOptionsModal({ - isOpen, - onModalClose, -}: ArtistIndexPosterOptionsModalProps) { - return ( - - - - ); -} - -export default ArtistIndexPosterOptionsModal; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js new file mode 100644 index 000000000..d0bc50baa --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js @@ -0,0 +1,248 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +const posterSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class ArtistIndexPosterOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showTitle: props.showTitle, + showMonitored: props.showMonitored, + showQualityProfile: props.showQualityProfile, + showNextAlbum: props.showNextAlbum, + showSearchAction: props.showSearchAction + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showTitle, + showMonitored, + showQualityProfile, + showNextAlbum, + showSearchAction + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showTitle !== prevProps.showTitle) { + state.showTitle = showTitle; + } + + if (showMonitored !== prevProps.showMonitored) { + state.showMonitored = showMonitored; + } + + if (showQualityProfile !== prevProps.showQualityProfile) { + state.showQualityProfile = showQualityProfile; + } + + if (showNextAlbum !== prevProps.showNextAlbum) { + state.showNextAlbum = showNextAlbum; + } + + if (showSearchAction !== prevProps.showSearchAction) { + state.showSearchAction = showSearchAction; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangePosterOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangePosterOption({ [name]: value }); + }); + }; + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + detailedProgressBar, + size, + showTitle, + showMonitored, + showQualityProfile, + showNextAlbum, + showSearchAction + } = this.state; + + return ( + + + Poster Options + + + +
+ + + {translate('PosterSize')} + + + + + + + + {translate('DetailedProgressBar')} + + + + + + + + {translate('ShowName')} + + + + + + + + {translate('ShowMonitored')} + + + + + + + + {translate('ShowQualityProfile')} + + + + + + + + {translate('ShowNextAlbum')} + + + + + + + + {translate('ShowSearch')} + + + + +
+
+ + + + +
+ ); + } +} + +ArtistIndexPosterOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showTitle: PropTypes.bool.isRequired, + showMonitored: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + showNextAlbum: PropTypes.bool.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showSearchAction: PropTypes.bool.isRequired, + onChangePosterOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexPosterOptionsModalContent; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx deleted file mode 100644 index 2560d855a..000000000 --- a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import selectPosterOptions from 'Artist/Index/Posters/selectPosterOptions'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import { setArtistPosterOption } from 'Store/Actions/artistIndexActions'; -import translate from 'Utilities/String/translate'; - -const posterSizeOptions = [ - { - key: 'small', - get value() { - return translate('Small'); - }, - }, - { - key: 'medium', - get value() { - return translate('Medium'); - }, - }, - { - key: 'large', - get value() { - return translate('Large'); - }, - }, -]; - -interface ArtistIndexPosterOptionsModalContentProps { - onModalClose(...args: unknown[]): unknown; -} - -function ArtistIndexPosterOptionsModalContent( - props: ArtistIndexPosterOptionsModalContentProps -) { - const { onModalClose } = props; - - const posterOptions = useSelector(selectPosterOptions); - - const { - detailedProgressBar, - size, - showTitle, - showMonitored, - showQualityProfile, - showNextAlbum, - showSearchAction, - } = posterOptions; - - const dispatch = useDispatch(); - - const onPosterOptionChange = useCallback( - ({ name, value }: { name: string; value: unknown }) => { - dispatch(setArtistPosterOption({ [name]: value })); - }, - [dispatch] - ); - - return ( - - {translate('PosterOptions')} - - -
- - {translate('PosterSize')} - - - - - - {translate('DetailedProgressBar')} - - - - - - {translate('ShowName')} - - - - - - {translate('ShowMonitored')} - - - - - - {translate('ShowQualityProfile')} - - - - - - {translate('ShowNextAlbum')} - - - - - - {translate('ShowSearch')} - - - -
-
- - - - -
- ); -} - -export default ArtistIndexPosterOptionsModalContent; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js new file mode 100644 index 000000000..72af268ad --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistPosterOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexPosterOptionsModalContent from './ArtistIndexPosterOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex, + (artistIndex) => { + return artistIndex.posterOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangePosterOption(payload) { + dispatch(setArtistPosterOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexPosterOptionsModalContent); diff --git a/frontend/src/Artist/Index/Posters/selectPosterOptions.ts b/frontend/src/Artist/Index/Posters/selectPosterOptions.ts deleted file mode 100644 index 1a53a0add..000000000 --- a/frontend/src/Artist/Index/Posters/selectPosterOptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -const selectPosterOptions = createSelector( - (state: AppState) => state.artistIndex.posterOptions, - (posterOptions) => posterOptions -); - -export default selectPosterOptions; diff --git a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.css b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.css index 9b5777117..ce5313877 100644 --- a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.css +++ b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.css @@ -4,6 +4,7 @@ border-radius: 0; background-color: #5b5b5b; color: var(--white); + transition: width 200ms ease; } .progressBar { diff --git a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js new file mode 100644 index 000000000..27d5c6f77 --- /dev/null +++ b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ProgressBar from 'Components/ProgressBar'; +import { sizes } from 'Helpers/Props'; +import getProgressBarKind from 'Utilities/Artist/getProgressBarKind'; +import translate from 'Utilities/String/translate'; +import styles from './ArtistIndexProgressBar.css'; + +function ArtistIndexProgressBar(props) { + const { + monitored, + status, + trackCount, + trackFileCount, + totalTrackCount, + posterWidth, + detailedProgressBar + } = props; + + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + const text = `${trackFileCount} / ${trackCount}`; + + return ( + + ); +} + +ArtistIndexProgressBar.propTypes = { + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + trackCount: PropTypes.number.isRequired, + trackFileCount: PropTypes.number.isRequired, + totalTrackCount: PropTypes.number.isRequired, + posterWidth: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired +}; + +export default ArtistIndexProgressBar; diff --git a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.tsx b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.tsx deleted file mode 100644 index 2a8167b99..000000000 --- a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import createArtistQueueItemsDetailsSelector, { - ArtistQueueDetails, -} from 'Artist/Index/createArtistQueueDetailsSelector'; -import ProgressBar from 'Components/ProgressBar'; -import { sizes } from 'Helpers/Props'; -import getProgressBarKind from 'Utilities/Artist/getProgressBarKind'; -import translate from 'Utilities/String/translate'; -import styles from './ArtistIndexProgressBar.css'; - -interface ArtistIndexProgressBarProps { - artistId: number; - monitored: boolean; - status: string; - trackCount: number; - trackFileCount: number; - totalTrackCount: number; - width: number; - detailedProgressBar: boolean; - isStandalone: boolean; -} - -function ArtistIndexProgressBar(props: ArtistIndexProgressBarProps) { - const { - artistId, - monitored, - status, - trackCount, - trackFileCount, - totalTrackCount, - width, - detailedProgressBar, - isStandalone, - } = props; - - const queueDetails: ArtistQueueDetails = useSelector( - createArtistQueueItemsDetailsSelector(artistId) - ); - - const newDownloads = queueDetails.count - queueDetails.tracksWithFiles; - const progress = trackCount ? (trackFileCount / trackCount) * 100 : 100; - const text = newDownloads - ? `${trackFileCount} + ${newDownloads} / ${trackCount}` - : `${trackFileCount} / ${trackCount}`; - - return ( - 0 - )} - size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL} - showText={detailedProgressBar} - text={text} - title={translate('ArtistProgressBarText', { - trackFileCount, - trackCount, - totalTrackCount, - downloadingCount: queueDetails.count, - })} - width={width} - /> - ); -} - -export default ArtistIndexProgressBar; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css deleted file mode 100644 index 5f6ee37c1..000000000 --- a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css +++ /dev/null @@ -1,10 +0,0 @@ -.albums { - display: flex; - flex-wrap: wrap; -} - -.truncated { - align-self: center; - flex: 0 0 100%; - padding: 4px 6px; -} diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx deleted file mode 100644 index 255bd9ba4..000000000 --- a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx +++ /dev/null @@ -1,89 +0,0 @@ -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'; -import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions'; -import createArtistAlbumsSelector from 'Store/Selectors/createArtistAlbumsSelector'; -import translate from 'Utilities/String/translate'; -import AlbumStudioAlbum from './AlbumStudioAlbum'; -import styles from './AlbumDetails.css'; - -interface AlbumDetailsProps { - artistId: number; -} - -function AlbumDetails(props: AlbumDetailsProps) { - const { artistId } = props; - - const { - isFetching, - isPopulated, - error, - items: albums, - } = useSelector(createArtistAlbumsSelector(artistId)); - - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(fetchAlbums({ artistId })); - - return () => { - dispatch(clearAlbums()); - }; - }, [dispatch, artistId]); - - const latestAlbums = useMemo(() => { - const sortedAlbums = _.orderBy(albums, 'releaseDate', 'desc'); - - return sortedAlbums.slice(0, 20); - }, [albums]); - - return ( -
- {isFetching ? : null} - - {!isFetching && error ? ( - {translate('AlbumsLoadError')} - ) : null} - - {isPopulated && !error - ? latestAlbums.map((album) => { - const { - id: albumId, - title, - disambiguation, - albumType, - monitored, - statistics = {} as Statistics, - isSaving = false, - } = album; - - return ( - - ); - }) - : null} - - {latestAlbums.length < albums.length ? ( -
- {translate('AlbumStudioTruncated')} -
- ) : null} -
- ); -} - -export default AlbumDetails; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx deleted file mode 100644 index 3e7e0578f..000000000 --- a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import classNames from 'classnames'; -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { Statistics } from 'Album/Album'; -import MonitorToggleButton from 'Components/MonitorToggleButton'; -import { toggleAlbumsMonitored } from 'Store/Actions/albumActions'; -import translate from 'Utilities/String/translate'; -import styles from './AlbumStudioAlbum.css'; - -interface AlbumStudioAlbumProps { - artistId: number; - albumId: number; - title: string; - disambiguation?: string; - albumType: string; - monitored: boolean; - statistics: Statistics; - isSaving: boolean; -} - -function AlbumStudioAlbum(props: AlbumStudioAlbumProps) { - const { - albumId, - title, - disambiguation, - albumType, - monitored, - statistics = { - trackFileCount: 0, - totalTrackCount: 0, - percentOfTracks: 0, - }, - isSaving = false, - } = props; - - const { - trackFileCount = 0, - totalTrackCount = 0, - percentOfTracks = 0, - } = statistics; - - const dispatch = useDispatch(); - const onAlbumMonitoredPress = useCallback(() => { - dispatch( - toggleAlbumsMonitored({ - albumIds: [albumId], - monitored: !monitored, - }) - ); - }, [albumId, monitored, dispatch]); - - return ( -
-
- - - - {disambiguation ? `${title} (${disambiguation})` : `${title}`} - -
- -
- {albumType} -
- -
- {totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}`} -
-
- ); -} - -export default AlbumStudioAlbum; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModal.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModal.tsx deleted file mode 100644 index b48717af0..000000000 --- a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModal.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ChangeMonitoringModalContent from './ChangeMonitoringModalContent'; - -interface ChangeMonitoringModalProps { - isOpen: boolean; - artistIds: number[]; - onSavePress(monitor: string): void; - onModalClose(): void; -} - -function ChangeMonitoringModal(props: ChangeMonitoringModalProps) { - const { isOpen, artistIds, onSavePress, onModalClose } = props; - - return ( - - - - ); -} - -export default ChangeMonitoringModal; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css deleted file mode 100644 index 29dc69dc4..000000000 --- a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css +++ /dev/null @@ -1,26 +0,0 @@ -.labelIcon { - margin-left: 8px; -} - -.message { - composes: alert from '~Components/Alert.css'; - - margin-bottom: 30px; -} - -.modalFooter { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - justify-content: space-between; -} - -.selected { - font-weight: bold; -} - -@media only screen and (max-width: $breakpointExtraSmall) { - .modalFooter { - flex-direction: column; - gap: 10px; - } -} diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx deleted file mode 100644 index b3c2abbbe..000000000 --- a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent'; -import Alert from 'Components/Alert'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import Popover from 'Components/Tooltip/Popover'; -import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './ChangeMonitoringModalContent.css'; - -const NO_CHANGE = 'noChange'; - -interface ChangeMonitoringModalContentProps { - artistIds: number[]; - saveError?: object; - onSavePress(monitor: string): void; - onModalClose(): void; -} - -function ChangeMonitoringModalContent( - props: ChangeMonitoringModalContentProps -) { - const { artistIds, onSavePress, onModalClose, ...otherProps } = props; - - const [monitor, setMonitor] = useState(NO_CHANGE); - - const onInputChange = useCallback( - ({ value }: { value: string }) => { - setMonitor(value); - }, - [setMonitor] - ); - - const onSavePressWrapper = useCallback(() => { - onSavePress(monitor); - }, [monitor, onSavePress]); - - const selectedCount = artistIds.length; - - return ( - - {translate('MonitorArtists')} - - - - {translate('MonitorAlbumExistingOnlyWarning')} - - -
- - - {translate('MonitorExistingAlbums')} - - } - title={translate('MonitoringOptions')} - body={} - position={tooltipPositions.RIGHT} - /> - - - - -
-
- - -
- {translate('CountArtistsSelected', { count: selectedCount })} -
- -
- - - -
-
-
- ); -} - -export default ChangeMonitoringModalContent; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css b/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css deleted file mode 100644 index eccb80f87..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css +++ /dev/null @@ -1,38 +0,0 @@ -.checkButton { - position: absolute; - top: 0; - left: 0; - z-index: 3; - width: 36px; - height: 36px; -} - -.checkContainer { - position: absolute; - top: 8px; - left: 8px; - width: 20px; - height: 20px; - border-radius: 50%; - background-color: var(--defaultColor); -} - -.selected { - color: var(--lidarrGreen); -} - -.unselected { - color: var(--white); -} - -.checkButton { - &:hover { - .selected { - color: var(--white); - } - - .unselected { - color: var(--lidarrGreen); - } - } -} diff --git a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css.d.ts b/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css.d.ts deleted file mode 100644 index d4de0b5f5..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.css.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'checkButton': string; - 'checkContainer': string; - 'selected': string; - 'unselected': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx b/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx deleted file mode 100644 index 86b41e8ba..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexPosterSelect.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { SyntheticEvent, useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import { icons } from 'Helpers/Props'; -import styles from './ArtistIndexPosterSelect.css'; - -interface ArtistIndexPosterSelectProps { - artistId: number; -} - -function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) { - const { artistId } = props; - const [selectState, selectDispatch] = useSelect(); - const isSelected = selectState.selectedState[artistId]; - - const onSelectPress = useCallback( - (event: SyntheticEvent) => { - const nativeEvent = event.nativeEvent as PointerEvent; - const shiftKey = nativeEvent.shiftKey; - - selectDispatch({ - type: 'toggleSelected', - id: artistId, - isSelected: !isSelected, - shiftKey, - }); - }, - [artistId, isSelected, selectDispatch] - ); - - return ( - - - - - - ); -} - -export default ArtistIndexPosterSelect; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx deleted file mode 100644 index 2b3e9c01c..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectAllButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import { icons } from 'Helpers/Props'; - -interface ArtistIndexSelectAllButtonProps { - label: string; - isSelectMode: boolean; - overflowComponent: React.FunctionComponent; -} - -function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) { - const { isSelectMode } = props; - const [selectState, selectDispatch] = useSelect(); - const { allSelected, allUnselected } = selectState; - - let icon = icons.SQUARE_MINUS; - - if (allSelected) { - icon = icons.CHECK_SQUARE; - } else if (allUnselected) { - icon = icons.SQUARE; - } - - const onPress = useCallback(() => { - selectDispatch({ - type: allSelected ? 'unselectAll' : 'selectAll', - }); - }, [allSelected, selectDispatch]); - - return isSelectMode ? ( - - ) : null; -} - -export default ArtistIndexSelectAllButton; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectAllMenuItem.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectAllMenuItem.tsx deleted file mode 100644 index 2340b65b6..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectAllMenuItem.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; -import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem'; -import { icons } from 'Helpers/Props'; - -interface ArtistIndexSelectAllMenuItemProps { - label: string; - isSelectMode: boolean; -} - -function ArtistIndexSelectAllMenuItem( - props: ArtistIndexSelectAllMenuItemProps -) { - const { isSelectMode } = props; - const [selectState, selectDispatch] = useSelect(); - const { allSelected, allUnselected } = selectState; - - let iconName = icons.SQUARE_MINUS; - - if (allSelected) { - iconName = icons.CHECK_SQUARE; - } else if (allUnselected) { - iconName = icons.SQUARE; - } - - const onPressWrapper = useCallback(() => { - selectDispatch({ - type: allSelected ? 'unselectAll' : 'selectAll', - }); - }, [allSelected, selectDispatch]); - - return isSelectMode ? ( - - ) : null; -} - -export default ArtistIndexSelectAllMenuItem; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css deleted file mode 100644 index d385923ef..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css +++ /dev/null @@ -1,72 +0,0 @@ -.footer { - composes: contentFooter from '~Components/Page/PageContentFooter.css'; - - align-items: center; -} - -.buttons { - display: flex; -} - -.actionButtons, -.deleteButtons { - display: flex; - gap: 10px; -} - -.deleteButtons { - margin-left: 50px; -} - -.selected { - display: flex; - justify-content: flex-end; - flex-grow: 1; - font-weight: bold; -} - -@media only screen and (max-width: $breakpointMedium) { - .buttons { - justify-content: center; - width: 100%; - } - - .selected { - justify-content: center; - margin-bottom: 20px; - width: 100%; - order: -1; - } -} - -@media only screen and (max-width: $breakpointSmall) { - .footer { - display: flex; - flex-direction: column; - } - - .buttons { - flex-direction: column; - margin-top: 20px; - gap: 20px; - } - - .actionButtons { - flex-wrap: wrap; - } - - .actionButtons, - .deleteButtons { - display: flex; - justify-content: center; - } - - .deleteButtons { - margin-left: 0; - } - - .selected { - justify-content: center; - order: -1; - } -} diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css.d.ts b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css.d.ts deleted file mode 100644 index 7f02229e3..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'actionButtons': string; - 'buttons': string; - 'deleteButtons': string; - 'footer': string; - 'selected': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx deleted file mode 100644 index f0569d607..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import { useSelect } from 'App/SelectContext'; -import AppState from 'App/State/AppState'; -import { RENAME_ARTIST, RETAG_ARTIST } from 'Commands/commandNames'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import PageContentFooter from 'Components/Page/PageContentFooter'; -import usePrevious from 'Helpers/Hooks/usePrevious'; -import { kinds } from 'Helpers/Props'; -import { - saveArtistEditor, - updateArtistsMonitor, -} from 'Store/Actions/artistActions'; -import { fetchRootFolders } from 'Store/Actions/settingsActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import ChangeMonitoringModal from './AlbumStudio/ChangeMonitoringModal'; -import RetagArtistModal from './AudioTags/RetagArtistModal'; -import DeleteArtistModal from './Delete/DeleteArtistModal'; -import EditArtistModal from './Edit/EditArtistModal'; -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) => { - const { isSaving, isDeleting, deleteError } = artist; - - return { - isSaving, - isDeleting, - deleteError, - }; - } -); - -function ArtistIndexSelectFooter() { - const { isSaving, isDeleting, deleteError } = - useSelector(artistEditorSelector); - - const isOrganizingArtist = useSelector( - createCommandExecutingSelector(RENAME_ARTIST) - ); - const isRetaggingArtist = useSelector( - createCommandExecutingSelector(RETAG_ARTIST) - ); - - const dispatch = useDispatch(); - - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [isOrganizeModalOpen, setIsOrganizeModalOpen] = useState(false); - const [isRetaggingModalOpen, setIsRetaggingModalOpen] = useState(false); - const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); - const [isMonitoringModalOpen, setIsMonitoringModalOpen] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isSavingArtist, setIsSavingArtist] = useState(false); - const [isSavingTags, setIsSavingTags] = useState(false); - const [isSavingMonitoring, setIsSavingMonitoring] = useState(false); - const previousIsDeleting = usePrevious(isDeleting); - - const [selectState, selectDispatch] = useSelect(); - const { selectedState } = selectState; - - const artistIds = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = artistIds.length; - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onSavePress = useCallback( - (payload: SavePayload) => { - setIsSavingArtist(true); - setIsEditModalOpen(false); - - dispatch( - saveArtistEditor({ - ...payload, - artistIds, - }) - ); - }, - [artistIds, dispatch] - ); - - const onOrganizePress = useCallback(() => { - setIsOrganizeModalOpen(true); - }, [setIsOrganizeModalOpen]); - - const onOrganizeModalClose = useCallback(() => { - setIsOrganizeModalOpen(false); - }, [setIsOrganizeModalOpen]); - - const onRetagPress = useCallback(() => { - setIsRetaggingModalOpen(true); - }, [setIsRetaggingModalOpen]); - - const onRetagModalClose = useCallback(() => { - setIsRetaggingModalOpen(false); - }, [setIsRetaggingModalOpen]); - - const onTagsPress = useCallback(() => { - setIsTagsModalOpen(true); - }, [setIsTagsModalOpen]); - - const onTagsModalClose = useCallback(() => { - setIsTagsModalOpen(false); - }, [setIsTagsModalOpen]); - - const onApplyTagsPress = useCallback( - (tags: number[], applyTags: string) => { - setIsSavingTags(true); - setIsTagsModalOpen(false); - - dispatch( - saveArtistEditor({ - artistIds, - tags, - applyTags, - }) - ); - }, - [artistIds, dispatch] - ); - - const onMonitoringPress = useCallback(() => { - setIsMonitoringModalOpen(true); - }, [setIsMonitoringModalOpen]); - - const onMonitoringClose = useCallback(() => { - setIsMonitoringModalOpen(false); - }, [setIsMonitoringModalOpen]); - - const onMonitoringSavePress = useCallback( - (monitor: string) => { - setIsSavingMonitoring(true); - setIsMonitoringModalOpen(false); - - dispatch( - updateArtistsMonitor({ - artistIds, - monitor, - }) - ); - }, - [artistIds, dispatch] - ); - - const onDeletePress = useCallback(() => { - setIsDeleteModalOpen(true); - }, [setIsDeleteModalOpen]); - - const onDeleteModalClose = useCallback(() => { - setIsDeleteModalOpen(false); - }, []); - - useEffect(() => { - if (!isSaving) { - setIsSavingArtist(false); - setIsSavingTags(false); - setIsSavingMonitoring(false); - } - }, [isSaving]); - - useEffect(() => { - if (previousIsDeleting && !isDeleting && !deleteError) { - selectDispatch({ type: 'unselectAll' }); - } - }, [previousIsDeleting, isDeleting, deleteError, selectDispatch]); - - useEffect(() => { - dispatch(fetchRootFolders()); - }, [dispatch]); - - const anySelected = selectedCount > 0; - - return ( - -
-
- - {translate('Edit')} - - - - {translate('RenameFiles')} - - - - {translate('WriteMetadataTags')} - - - - {translate('SetAppTags')} - - - - {translate('UpdateMonitoring')} - -
- -
- - {translate('Delete')} - -
-
- -
- {translate('CountArtistsSelected', { count: selectedCount })} -
- - - - - - - - - - - - -
- ); -} - -export default ArtistIndexSelectFooter; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx deleted file mode 100644 index 8679bba99..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { IconDefinition } from '@fortawesome/fontawesome-common-types'; -import React, { useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; - -interface ArtistIndexSelectModeButtonProps { - label: string; - iconName: IconDefinition; - isSelectMode: boolean; - overflowComponent: React.FunctionComponent; - onPress: () => void; -} - -function ArtistIndexSelectModeButton(props: ArtistIndexSelectModeButtonProps) { - const { label, iconName, isSelectMode, onPress } = props; - const [, selectDispatch] = useSelect(); - - const onPressWrapper = useCallback(() => { - if (isSelectMode) { - selectDispatch({ - type: 'reset', - }); - } - - onPress(); - }, [isSelectMode, onPress, selectDispatch]); - - return ( - - ); -} - -export default ArtistIndexSelectModeButton; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeMenuItem.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectModeMenuItem.tsx deleted file mode 100644 index b5a7a6de4..000000000 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectModeMenuItem.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { IconDefinition } from '@fortawesome/fontawesome-common-types'; -import React, { useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; -import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem'; - -interface ArtistIndexSelectModeMenuItemProps { - label: string; - iconName: IconDefinition; - isSelectMode: boolean; - onPress: () => void; -} - -function ArtistIndexSelectModeMenuItem( - props: ArtistIndexSelectModeMenuItemProps -) { - const { label, iconName, isSelectMode, onPress } = props; - const [, selectDispatch] = useSelect(); - - const onPressWrapper = useCallback(() => { - if (isSelectMode) { - selectDispatch({ - type: 'reset', - }); - } - - onPress(); - }, [isSelectMode, onPress, selectDispatch]); - - return ( - - ); -} - -export default ArtistIndexSelectModeMenuItem; diff --git a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModal.tsx b/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModal.tsx deleted file mode 100644 index 5d5f1fb6a..000000000 --- a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModal.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import RetagArtistModalContent from './RetagArtistModalContent'; - -interface RetagArtistModalProps { - isOpen: boolean; - artistIds: number[]; - onModalClose: () => void; -} - -function RetagArtistModal(props: RetagArtistModalProps) { - const { isOpen, onModalClose, ...otherProps } = props; - - return ( - - - - ); -} - -export default RetagArtistModal; diff --git a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx b/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx deleted file mode 100644 index b67ee60aa..000000000 --- a/frontend/src/Artist/Index/Select/AudioTags/RetagArtistModalContent.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { orderBy } from 'lodash'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Artist from 'Artist/Artist'; -import { RETAG_ARTIST } from 'Commands/commandNames'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { icons, kinds } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; -import translate from 'Utilities/String/translate'; -import styles from './RetagArtistModalContent.css'; - -interface RetagArtistModalContentProps { - artistIds: number[]; - onModalClose: () => void; -} - -function RetagArtistModalContent(props: RetagArtistModalContentProps) { - const { artistIds, onModalClose } = props; - - const allArtists: Artist[] = useSelector(createAllArtistSelector()); - 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 sorted = orderBy(artists, ['sortName']); - - return sorted.map((a) => a.artistName); - }, [artistIds, allArtists]); - - const onRetagPress = useCallback(() => { - dispatch( - executeCommand({ - name: RETAG_ARTIST, - artistIds, - }) - ); - - onModalClose(); - }, [artistIds, onModalClose, dispatch]); - - return ( - - {translate('RetagSelectedArtists')} - - - - Tip: To preview the tags that will be written, select "Cancel", then - select any artist name and use the - - - -
- Are you sure you want to retag all files in the {artistNames.length}{' '} - selected artist? -
- -
    - {artistNames.map((artistName) => { - return
  • {artistName}
  • ; - })} -
-
- - - - - - -
- ); -} - -export default RetagArtistModalContent; diff --git a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModal.tsx b/frontend/src/Artist/Index/Select/Delete/DeleteArtistModal.tsx deleted file mode 100644 index c909d7406..000000000 --- a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModal.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import DeleteArtistModalContent from './DeleteArtistModalContent'; - -interface DeleteArtistModalProps { - isOpen: boolean; - artistIds: number[]; - onModalClose(): void; -} - -function DeleteArtistModal(props: DeleteArtistModalProps) { - const { isOpen, artistIds, onModalClose } = props; - - return ( - - - - ); -} - -export default DeleteArtistModal; diff --git a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx b/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx deleted file mode 100644 index 4accc9f0e..000000000 --- a/frontend/src/Artist/Index/Select/Delete/DeleteArtistModalContent.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { orderBy } from 'lodash'; -import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds } 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'; - -interface DeleteArtistModalContentProps { - artistIds: number[]; - onModalClose(): void; -} - -const selectDeleteOptions = createSelector( - (state: AppState) => state.artist.deleteOptions, - (deleteOptions) => deleteOptions -); - -function DeleteArtistModalContent(props: DeleteArtistModalContentProps) { - const { artistIds, onModalClose } = props; - - const { addImportListExclusion } = useSelector(selectDeleteOptions); - const allArtists: Artist[] = useSelector(createAllArtistSelector()); - const dispatch = useDispatch(); - - const [deleteFiles, setDeleteFiles] = useState(false); - - const artists = useMemo((): Artist[] => { - const artistList = artistIds.map((id) => { - return allArtists.find((a) => a.id === id); - }) as Artist[]; - - return orderBy(artistList, ['sortName']); - }, [artistIds, allArtists]); - - const onDeleteFilesChange = useCallback( - ({ value }: CheckInputChanged) => { - setDeleteFiles(value); - }, - [setDeleteFiles] - ); - - const onDeleteOptionChange = useCallback( - ({ name, value }: { name: string; value: boolean }) => { - dispatch( - setDeleteOption({ - [name]: value, - }) - ); - }, - [dispatch] - ); - - const onDeleteArtistConfirmed = useCallback(() => { - setDeleteFiles(false); - - dispatch( - bulkDeleteArtist({ - artistIds, - deleteFiles, - addImportListExclusion, - }) - ); - - onModalClose(); - }, [ - artistIds, - deleteFiles, - addImportListExclusion, - setDeleteFiles, - dispatch, - onModalClose, - ]); - - return ( - - {translate('DeleteSelectedArtists')} - - -
- - {translate('AddListExclusion')} - - - - - - - {artists.length > 1 - ? translate('DeleteArtistFolders') - : translate('DeleteArtistFolder')} - - - 1 - ? translate('DeleteArtistFoldersHelpText') - : translate('DeleteArtistFolderHelpText') - } - kind={kinds.DANGER} - onChange={onDeleteFilesChange} - /> - -
- -
- {deleteFiles - ? translate('DeleteArtistFolderCountWithFilesConfirmation', { - count: artists.length, - }) - : translate('DeleteArtistFolderCountConfirmation', { - count: artists.length, - })} -
- -
    - {artists.map((a) => { - return ( -
  • - {a.artistName} - - {deleteFiles && ( - - -{a.path} - - )} -
  • - ); - })} -
-
- - - - - - -
- ); -} - -export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Index/Select/Edit/EditArtistModal.tsx b/frontend/src/Artist/Index/Select/Edit/EditArtistModal.tsx deleted file mode 100644 index bdb6726be..000000000 --- a/frontend/src/Artist/Index/Select/Edit/EditArtistModal.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import EditArtistModalContent from './EditArtistModalContent'; - -interface EditArtistModalProps { - isOpen: boolean; - artistIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -function EditArtistModal(props: EditArtistModalProps) { - const { isOpen, artistIds, onSavePress, onModalClose } = props; - - return ( - - - - ); -} - -export default EditArtistModal; diff --git a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.css b/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.css deleted file mode 100644 index ea406894e..000000000 --- a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.modalFooter { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - justify-content: space-between; -} - -.selected { - font-weight: bold; -} - -@media only screen and (max-width: $breakpointExtraSmall) { - .modalFooter { - flex-direction: column; - gap: 10px; - } -} diff --git a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.css.d.ts b/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.css.d.ts deleted file mode 100644 index cbf2d6328..000000000 --- a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'modalFooter': string; - 'selected': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx b/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx deleted file mode 100644 index 993be8ce5..000000000 --- a/frontend/src/Artist/Index/Select/Edit/EditArtistModalContent.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './EditArtistModalContent.css'; - -interface SavePayload { - monitored?: boolean; - monitorNewItems?: string; - qualityProfileId?: number; - metadataProfileId?: number; - rootFolderPath?: string; - moveFiles?: boolean; -} - -interface EditArtistModalContentProps { - artistIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -const NO_CHANGE = 'noChange'; - -const monitoredOptions = [ - { - key: NO_CHANGE, - get value() { - return translate('NoChange'); - }, - isDisabled: true, - }, - { - key: 'monitored', - get value() { - return translate('Monitored'); - }, - }, - { - key: 'unmonitored', - get value() { - return translate('Unmonitored'); - }, - }, -]; - -function EditArtistModalContent(props: EditArtistModalContentProps) { - const { artistIds, onSavePress, onModalClose } = props; - - const [monitored, setMonitored] = useState(NO_CHANGE); - const [monitorNewItems, setMonitorNewItems] = useState(NO_CHANGE); - const [qualityProfileId, setQualityProfileId] = useState( - NO_CHANGE - ); - const [metadataProfileId, setMetadataProfileId] = useState( - NO_CHANGE - ); - const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE); - const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false); - - const save = useCallback( - (moveFiles: boolean) => { - let hasChanges = false; - const payload: SavePayload = {}; - - if (monitored !== NO_CHANGE) { - hasChanges = true; - payload.monitored = monitored === 'monitored'; - } - - if (monitorNewItems !== NO_CHANGE) { - hasChanges = true; - payload.monitorNewItems = monitorNewItems; - } - - if (qualityProfileId !== NO_CHANGE) { - hasChanges = true; - payload.qualityProfileId = qualityProfileId as number; - } - - if (metadataProfileId !== NO_CHANGE) { - hasChanges = true; - payload.metadataProfileId = metadataProfileId as number; - } - - if (rootFolderPath !== NO_CHANGE) { - hasChanges = true; - payload.rootFolderPath = rootFolderPath; - payload.moveFiles = moveFiles; - } - - if (hasChanges) { - onSavePress(payload); - } - - onModalClose(); - }, - [ - monitored, - monitorNewItems, - qualityProfileId, - metadataProfileId, - rootFolderPath, - onSavePress, - onModalClose, - ] - ); - - const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { - switch (name) { - case 'monitored': - setMonitored(value); - break; - case 'monitorNewItems': - setMonitorNewItems(value); - break; - case 'qualityProfileId': - setQualityProfileId(value); - break; - case 'metadataProfileId': - setMetadataProfileId(value); - break; - case 'rootFolderPath': - setRootFolderPath(value); - break; - default: - console.warn('EditArtistModalContent Unknown Input'); - } - }, - [setMonitored] - ); - - const onSavePressWrapper = useCallback(() => { - if (rootFolderPath === NO_CHANGE) { - save(false); - } else { - setIsConfirmMoveModalOpen(true); - } - }, [rootFolderPath, save]); - - const onCancelPress = useCallback(() => { - setIsConfirmMoveModalOpen(false); - }, [setIsConfirmMoveModalOpen]); - - const onDoNotMoveArtistPress = useCallback(() => { - setIsConfirmMoveModalOpen(false); - save(false); - }, [setIsConfirmMoveModalOpen, save]); - - const onMoveArtistPress = useCallback(() => { - setIsConfirmMoveModalOpen(false); - save(true); - }, [setIsConfirmMoveModalOpen, save]); - - const selectedCount = artistIds.length; - - return ( - - {translate('EditSelectedArtists')} - - - - {translate('Monitored')} - - - - - - {translate('MonitorNewItems')} - - - - - - {translate('QualityProfile')} - - - - - - {translate('MetadataProfile')} - - - - - - {translate('RootFolder')} - - - - - - -
- {translate('CountArtistsSelected', { count: selectedCount })} -
- -
- - - -
-
- - -
- ); -} - -export default EditArtistModalContent; diff --git a/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModal.tsx b/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModal.tsx deleted file mode 100644 index bec35222b..000000000 --- a/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModal.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import OrganizeArtistModalContent from './OrganizeArtistModalContent'; - -interface OrganizeArtistModalProps { - isOpen: boolean; - artistIds: number[]; - onModalClose: () => void; -} - -function OrganizeArtistModal(props: OrganizeArtistModalProps) { - const { isOpen, onModalClose, ...otherProps } = props; - - return ( - - - - ); -} - -export default OrganizeArtistModal; diff --git a/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModalContent.tsx b/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModalContent.tsx deleted file mode 100644 index 8184abba7..000000000 --- a/frontend/src/Artist/Index/Select/Organize/OrganizeArtistModalContent.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { orderBy } from 'lodash'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Artist from 'Artist/Artist'; -import { RENAME_ARTIST } from 'Commands/commandNames'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { icons, kinds } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; -import translate from 'Utilities/String/translate'; -import styles from './OrganizeArtistModalContent.css'; - -interface OrganizeArtistModalContentProps { - artistIds: number[]; - onModalClose: () => void; -} - -function OrganizeArtistModalContent(props: OrganizeArtistModalContentProps) { - const { artistIds, onModalClose } = props; - - const allArtists: Artist[] = useSelector(createAllArtistSelector()); - 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 sorted = orderBy(artists, ['sortName']); - - return sorted.map((a) => a.artistName); - }, [artistIds, allArtists]); - - const onOrganizePress = useCallback(() => { - dispatch( - executeCommand({ - name: RENAME_ARTIST, - artistIds, - }) - ); - - onModalClose(); - }, [artistIds, onModalClose, dispatch]); - - return ( - - {translate('OrganizeSelectedArtists')} - - - - Tip: To preview a rename, select "Cancel", then select any artist name - and use the - - - -
- Are you sure you want to organize all files in the{' '} - {artistNames.length} selected artist? -
- -
    - {artistNames.map((artistName) => { - return
  • {artistName}
  • ; - })} -
-
- - - - - - -
- ); -} - -export default OrganizeArtistModalContent; diff --git a/frontend/src/Artist/Index/Select/Tags/TagsModal.tsx b/frontend/src/Artist/Index/Select/Tags/TagsModal.tsx deleted file mode 100644 index 8635867e4..000000000 --- a/frontend/src/Artist/Index/Select/Tags/TagsModal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import TagsModalContent from './TagsModalContent'; - -interface TagsModalProps { - isOpen: boolean; - artistIds: number[]; - onApplyTagsPress: (tags: number[], applyTags: string) => void; - onModalClose: () => void; -} - -function TagsModal(props: TagsModalProps) { - const { isOpen, onModalClose, ...otherProps } = props; - - return ( - - - - ); -} - -export default TagsModal; diff --git a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx deleted file mode 100644 index 95a7eaae2..000000000 --- a/frontend/src/Artist/Index/Select/Tags/TagsModalContent.tsx +++ /dev/null @@ -1,189 +0,0 @@ -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'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Label from 'Components/Label'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import translate from 'Utilities/String/translate'; -import styles from './TagsModalContent.css'; - -interface TagsModalContentProps { - artistIds: number[]; - onApplyTagsPress: (tags: number[], applyTags: string) => void; - onModalClose: () => void; -} - -function TagsModalContent(props: TagsModalContentProps) { - const { artistIds, onModalClose, onApplyTagsPress } = props; - - const allArtists: Artist[] = useSelector(createAllArtistSelector()); - const tagList: Tag[] = useSelector(createTagsSelector()); - - const [tags, setTags] = useState([]); - const [applyTags, setApplyTags] = useState('add'); - - const artistTags = useMemo(() => { - const tags = artistIds.reduce((acc: number[], id) => { - const a = allArtists.find((a) => a.id === id); - - if (a) { - acc.push(...a.tags); - } - - return acc; - }, []); - - return uniq(tags); - }, [artistIds, allArtists]); - - const onTagsChange = useCallback( - ({ value }: { value: number[] }) => { - setTags(value); - }, - [setTags] - ); - - const onApplyTagsChange = useCallback( - ({ value }: { value: string }) => { - setApplyTags(value); - }, - [setApplyTags] - ); - - const onApplyPress = useCallback(() => { - onApplyTagsPress(tags, applyTags); - }, [tags, applyTags, onApplyTagsPress]); - - const applyTagsOptions = [ - { - key: 'add', - value: translate('Add'), - }, - { - key: 'remove', - value: translate('Remove'), - }, - { - key: 'replace', - value: translate('Replace'), - }, - ]; - - return ( - - {translate('Tags')} - - -
- - {translate('Tags')} - - - - - - {translate('ApplyTags')} - - - - - - {translate('Result')} - -
- {artistTags.map((id) => { - const tag = tagList.find((t) => t.id === id); - - if (!tag) { - return null; - } - - const removeTag = - (applyTags === 'remove' && tags.indexOf(id) > -1) || - (applyTags === 'replace' && tags.indexOf(id) === -1); - - return ( - - ); - })} - - {(applyTags === 'add' || applyTags === 'replace') && - tags.map((id) => { - const tag = tagList.find((t) => t.id === id); - - if (!tag) { - return null; - } - - if (artistTags.indexOf(id) > -1) { - return null; - } - - return ( - - ); - })} -
-
-
-
- - - - - - -
- ); -} - -export default TagsModalContent; diff --git a/frontend/src/Artist/Index/Table/AlbumsCell.css b/frontend/src/Artist/Index/Table/AlbumsCell.css deleted file mode 100644 index 307c5d406..000000000 --- a/frontend/src/Artist/Index/Table/AlbumsCell.css +++ /dev/null @@ -1,4 +0,0 @@ -.albumCount { - width: 100%; - cursor: default; -} diff --git a/frontend/src/Artist/Index/Table/AlbumsCell.tsx b/frontend/src/Artist/Index/Table/AlbumsCell.tsx deleted file mode 100644 index b42298b5d..000000000 --- a/frontend/src/Artist/Index/Table/AlbumsCell.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import AlbumDetails from 'Artist/Index/Select/AlbumStudio/AlbumDetails'; -import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; -import Popover from 'Components/Tooltip/Popover'; -import TooltipPosition from 'Helpers/Props/TooltipPosition'; -import translate from 'Utilities/String/translate'; -import styles from './AlbumsCell.css'; - -interface SeriesStatusCellProps { - className: string; - artistId: number; - albumCount: number; - isSelectMode: boolean; -} - -function AlbumsCell(props: SeriesStatusCellProps) { - const { className, artistId, albumCount, isSelectMode, ...otherProps } = - props; - - return ( - - {isSelectMode && albumCount > 0 ? ( - } - position={TooltipPosition.Left} - canFlip={true} - /> - ) : ( - albumCount - )} - - ); -} - -export default AlbumsCell; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js new file mode 100644 index 000000000..a2a3c8dab --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +class ArtistIndexActionsCell extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + }; + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + }; + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + }; + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + }; + + // + // Render + + render() { + const { + id, + isRefreshingArtist, + onRefreshArtistPress, + ...otherProps + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + return ( + + + + + + + + + + ); + } +} + +ArtistIndexActionsCell.propTypes = { + id: PropTypes.number.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired +}; + +export default ArtistIndexActionsCell; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css similarity index 98% rename from frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css rename to frontend/src/Artist/Index/Table/ArtistIndexHeader.css index 7ea4e94aa..6da0be920 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css @@ -31,7 +31,6 @@ flex: 1 0 125px; } -.monitorNewItems, .nextAlbum, .lastAlbum, .added, diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css.d.ts b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css.d.ts similarity index 95% rename from frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css.d.ts rename to frontend/src/Artist/Index/Table/ArtistIndexHeader.css.d.ts index 467b401bb..4d9dcd20b 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.css.d.ts +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css.d.ts @@ -11,7 +11,6 @@ interface CssExports { 'lastAlbum': string; 'latestAlbum': string; 'metadataProfileId': string; - 'monitorNewItems': string; 'nextAlbum': string; 'path': string; 'qualityProfileId': string; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.js b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js new file mode 100644 index 000000000..7054bbaf3 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js @@ -0,0 +1,86 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import IconButton from 'Components/Link/IconButton'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; +import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; +import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; +import { icons } from 'Helpers/Props'; +import ArtistIndexTableOptionsConnector from './ArtistIndexTableOptionsConnector'; +import hasGrowableColumns from './hasGrowableColumns'; +import styles from './ArtistIndexHeader.css'; + +function ArtistIndexHeader(props) { + const { + showBanners, + columns, + onTableOptionChange, + ...otherProps + } = props; + + return ( + + { + columns.map((column) => { + const { + name, + label, + isSortable, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'actions') { + return ( + + + + + + + ); + } + + return ( + + {typeof label === 'function' ? label() : label} + + ); + }) + } + + ); +} + +ArtistIndexHeader.propTypes = { + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onTableOptionChange: PropTypes.func.isRequired, + showBanners: PropTypes.bool.isRequired +}; + +export default ArtistIndexHeader; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js new file mode 100644 index 000000000..37ddd9ef3 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { setArtistTableOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexHeader from './ArtistIndexHeader'; + +function createMapDispatchToProps(dispatch, props) { + return { + onTableOptionChange(payload) { + dispatch(setArtistTableOption(payload)); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(ArtistIndexHeader); diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css b/frontend/src/Artist/Index/Table/ArtistIndexRow.css index 35d03c263..b75ad6afd 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.css +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css @@ -67,7 +67,6 @@ flex: 1 0 125px; } -.monitorNewItems, .nextAlbum, .lastAlbum, .added, diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts b/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts index 4855aec75..fd8d84e17 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css.d.ts @@ -14,7 +14,6 @@ interface CssExports { 'lastAlbum': string; 'link': string; 'metadataProfileId': string; - 'monitorNewItems': string; 'nextAlbum': string; 'overlayTitle': string; 'path': string; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.js b/frontend/src/Artist/Index/Table/ArtistIndexRow.js new file mode 100644 index 000000000..0dc5585ca --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.js @@ -0,0 +1,487 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import AlbumTitleLink from 'Album/AlbumTitleLink'; +import ArtistBanner from 'Artist/ArtistBanner'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import HeartRating from 'Components/HeartRating'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import ProgressBar from 'Components/ProgressBar'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import TagListConnector from 'Components/TagListConnector'; +import { icons } from 'Helpers/Props'; +import getProgressBarKind from 'Utilities/Artist/getProgressBarKind'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; +import ArtistStatusCell from './ArtistStatusCell'; +import hasGrowableColumns from './hasGrowableColumns'; +import styles from './ArtistIndexRow.css'; + +class ArtistIndexRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasBannerError: false, + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + }; + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + }; + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + }; + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + }; + + onUseSceneNumberingChange = () => { + // Mock handler to satisfy `onChange` being required for `CheckInput`. + // + }; + + onBannerLoad = () => { + if (this.state.hasBannerError) { + this.setState({ hasBannerError: false }); + } + }; + + onBannerLoadError = () => { + if (!this.state.hasBannerError) { + this.setState({ hasBannerError: true }); + } + }; + + // + // Render + + render() { + const { + id, + monitored, + status, + artistName, + foreignArtistId, + artistType, + qualityProfile, + metadataProfile, + nextAlbum, + lastAlbum, + added, + statistics, + genres, + ratings, + path, + tags, + images, + isSaving, + showBanners, + showSearchAction, + columns, + isRefreshingArtist, + isSearchingArtist, + onRefreshArtistPress, + onSearchPress, + onMonitoredPress + } = this.props; + + const { + albumCount, + trackCount, + trackFileCount, + totalTrackCount, + sizeOnDisk + } = statistics; + + const { + hasBannerError, + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + return ( + <> + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'sortName') { + return ( + + { + showBanners ? + + + + { + hasBannerError && +
+ {artistName} +
+ } + : + + + } +
+ ); + } + + if (name === 'artistType') { + return ( + + {artistType} + + ); + } + + if (name === 'qualityProfileId') { + return ( + + {qualityProfile.name} + + ); + } + + if (name === 'metadataProfileId') { + return ( + + {metadataProfile.name} + + ); + } + + if (name === 'nextAlbum') { + if (nextAlbum) { + return ( + + + + ); + } + return ( + + None + + ); + } + + if (name === 'lastAlbum') { + if (lastAlbum) { + return ( + + + + ); + } + return ( + + None + + ); + } + + if (name === 'added') { + return ( + + ); + } + + if (name === 'albumCount') { + return ( + + {albumCount} + + ); + } + + if (name === 'trackProgress') { + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + + return ( + + + + ); + } + + if (name === 'trackCount') { + return ( + + {totalTrackCount} + + ); + } + + if (name === 'path') { + return ( + + {path} + + ); + } + + if (name === 'sizeOnDisk') { + return ( + + {formatBytes(sizeOnDisk)} + + ); + } + + if (name === 'genres') { + const joinedGenres = genres.join(', '); + + return ( + + + {joinedGenres} + + + ); + } + + if (name === 'ratings') { + return ( + + + + ); + } + + if (name === 'tags') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + + + { + showSearchAction && + + } + + + + ); + } + + return null; + }) + } + + + + + + ); + } +} + +ArtistIndexRow.propTypes = { + id: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + foreignArtistId: PropTypes.string.isRequired, + artistType: PropTypes.string, + qualityProfile: PropTypes.object.isRequired, + metadataProfile: PropTypes.object.isRequired, + nextAlbum: PropTypes.object, + lastAlbum: PropTypes.object, + added: PropTypes.string, + statistics: PropTypes.object.isRequired, + latestAlbum: PropTypes.object, + path: PropTypes.string.isRequired, + genres: PropTypes.arrayOf(PropTypes.string).isRequired, + ratings: PropTypes.object.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + isSaving: PropTypes.bool.isRequired, + showBanners: PropTypes.bool.isRequired, + showSearchAction: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + isSearchingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired, + onMonitoredPress: PropTypes.func.isRequired +}; + +ArtistIndexRow.defaultProps = { + statistics: { + albumCount: 0, + trackCount: 0, + trackFileCount: 0, + totalTrackCount: 0 + }, + genres: [], + tags: [] +}; + +export default ArtistIndexRow; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx deleted file mode 100644 index 0398f5502..000000000 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx +++ /dev/null @@ -1,422 +0,0 @@ -import classNames from 'classnames'; -import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AlbumTitleLink from 'Album/AlbumTitleLink'; -import { useSelect } from 'App/SelectContext'; -import { Statistics } from 'Artist/Artist'; -import ArtistBanner from 'Artist/ArtistBanner'; -import ArtistNameLink from 'Artist/ArtistNameLink'; -import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; -import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; -import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector'; -import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; -import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell'; -import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames'; -import HeartRating from 'Components/HeartRating'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; -import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; -import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; -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'; -import selectTableOptions from './selectTableOptions'; -import styles from './ArtistIndexRow.css'; - -interface ArtistIndexRowProps { - artistId: number; - sortKey: string; - columns: Column[]; - isSelectMode: boolean; -} - -function ArtistIndexRow(props: ArtistIndexRowProps) { - const { artistId, columns, isSelectMode } = props; - - const { - artist, - qualityProfile, - metadataProfile, - isRefreshingArtist, - isSearchingArtist, - } = useSelector(createArtistIndexItemSelector(props.artistId)); - - const { showBanners, showSearchAction } = useSelector(selectTableOptions); - - const { - artistName, - foreignArtistId, - monitored, - status, - path, - monitorNewItems, - nextAlbum, - lastAlbum, - added, - statistics = {} as Statistics, - images, - artistType, - genres = [], - ratings, - tags = [], - isSaving = false, - } = artist; - - const { - albumCount = 0, - trackCount = 0, - trackFileCount = 0, - totalTrackCount = 0, - sizeOnDisk = 0, - } = statistics; - - const dispatch = useDispatch(); - const [hasBannerError, setHasBannerError] = useState(false); - const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false); - const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false); - const [selectState, selectDispatch] = useSelect(); - - const onRefreshPress = useCallback(() => { - dispatch( - executeCommand({ - name: REFRESH_ARTIST, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onSearchPress = useCallback(() => { - dispatch( - executeCommand({ - name: ARTIST_SEARCH, - artistId, - }) - ); - }, [artistId, dispatch]); - - const onBannerLoadError = useCallback(() => { - setHasBannerError(true); - }, [setHasBannerError]); - - const onBannerLoad = useCallback(() => { - setHasBannerError(false); - }, [setHasBannerError]); - - const onEditArtistPress = useCallback(() => { - setIsEditArtistModalOpen(true); - }, [setIsEditArtistModalOpen]); - - const onEditArtistModalClose = useCallback(() => { - setIsEditArtistModalOpen(false); - }, [setIsEditArtistModalOpen]); - - const onDeleteArtistPress = useCallback(() => { - setIsEditArtistModalOpen(false); - setIsDeleteArtistModalOpen(true); - }, [setIsDeleteArtistModalOpen]); - - const onDeleteArtistModalClose = useCallback(() => { - setIsDeleteArtistModalOpen(false); - }, [setIsDeleteArtistModalOpen]); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey }: SelectStateInputProps) => { - selectDispatch({ - type: 'toggleSelected', - id, - isSelected: value, - shiftKey, - }); - }, - [selectDispatch] - ); - - return ( - <> - {isSelectMode ? ( - - ) : null} - - {columns.map((column) => { - const { name, isVisible } = column; - - if (!isVisible) { - return null; - } - - if (name === 'status') { - return ( - - ); - } - - if (name === 'sortName') { - return ( - - {showBanners ? ( - - - - {hasBannerError && ( -
{artistName}
- )} - - ) : ( - - )} -
- ); - } - - if (name === 'artistType') { - return ( - - {artistType} - - ); - } - - if (name === 'qualityProfileId') { - return ( - - {qualityProfile?.name ?? ''} - - ); - } - - if (name === 'metadataProfileId') { - return ( - - {metadataProfile?.name ?? ''} - - ); - } - - if (name === 'monitorNewItems') { - return ( - - {translate(firstCharToUpper(monitorNewItems))} - - ); - } - - if (name === 'nextAlbum') { - if (nextAlbum) { - return ( - - - - ); - } - return ( - - {translate('None')} - - ); - } - - if (name === 'lastAlbum') { - if (lastAlbum) { - return ( - - - - ); - } - return ( - - {translate('None')} - - ); - } - - if (name === 'added') { - return ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ts(2739) - - ); - } - - if (name === 'albumCount') { - return ( - - ); - } - - if (name === 'trackProgress') { - return ( - - - - ); - } - - if (name === 'trackCount') { - return ( - - {totalTrackCount} - - ); - } - - if (name === 'path') { - return ( - - {path} - - ); - } - - if (name === 'sizeOnDisk') { - return ( - - {formatBytes(sizeOnDisk)} - - ); - } - - if (name === 'genres') { - const joinedGenres = genres.join(', '); - - return ( - - {joinedGenres} - - ); - } - - if (name === 'ratings') { - return ( - - - - ); - } - - if (name === 'tags') { - return ( - - - - ); - } - - if (name === 'actions') { - return ( - - - - {showSearchAction ? ( - - ) : null} - - - - ); - } - - return null; - })} - - - - - - ); -} - -export default ArtistIndexRow; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.css b/frontend/src/Artist/Index/Table/ArtistIndexTable.css index 0bfc5fec4..23ab127b5 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTable.css +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.css @@ -1,11 +1,5 @@ -.tableScroller { - position: relative; -} +.tableContainer { + composes: tableContainer from '~Components/Table/VirtualTable.css'; -.row { - transition: background-color 500ms; - - &:hover { - background-color: var(--tableRowHoverBackgroundColor); - } + flex: 1 0 auto; } diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.css.d.ts b/frontend/src/Artist/Index/Table/ArtistIndexTable.css.d.ts index ff35c263f..fbc2e3b9a 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTable.css.d.ts +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.css.d.ts @@ -1,8 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'row': string; - 'tableScroller': string; + 'tableContainer': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.js b/frontend/src/Artist/Index/Table/ArtistIndexTable.js new file mode 100644 index 000000000..00f6a80d1 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.js @@ -0,0 +1,134 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import VirtualTable from 'Components/Table/VirtualTable'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; +import { sortDirections } from 'Helpers/Props'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import ArtistIndexHeaderConnector from './ArtistIndexHeaderConnector'; +import ArtistIndexRow from './ArtistIndexRow'; +import styles from './ArtistIndexTable.css'; + +class ArtistIndexTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + scrollIndex: null + }; + } + + componentDidUpdate(prevProps) { + const { + items, + jumpToCharacter + } = this.props; + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + + const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (scrollIndex != null) { + this.setState({ scrollIndex }); + } + } else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) { + this.setState({ scrollIndex: null }); + } + } + + // + // Control + + rowRenderer = ({ key, rowIndex, style }) => { + const { + items, + columns, + showBanners, + isSaving + } = this.props; + + const artist = items[rowIndex]; + + return ( + + + + ); + }; + + // + // Render + + render() { + const { + items, + columns, + sortKey, + sortDirection, + showBanners, + isSmallScreen, + onSortPress, + scroller, + scrollTop + } = this.props; + + return ( + + } + columns={columns} + sortKey={sortKey} + sortDirection={sortDirection} + /> + ); + } +} + +ArtistIndexTable.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + showBanners: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + jumpToCharacter: PropTypes.string, + scrollTop: PropTypes.number, + scroller: PropTypes.instanceOf(Element).isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortPress: PropTypes.func.isRequired +}; + +export default ArtistIndexTable; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx b/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx deleted file mode 100644 index c3c8044ce..000000000 --- a/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { throttle } from 'lodash'; -import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow'; -import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader'; -import Scroller from 'Components/Scroller/Scroller'; -import Column from 'Components/Table/Column'; -import useMeasure from 'Helpers/Hooks/useMeasure'; -import ScrollDirection from 'Helpers/Props/ScrollDirection'; -import SortDirection from 'Helpers/Props/SortDirection'; -import dimensions from 'Styles/Variables/dimensions'; -import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; -import selectTableOptions from './selectTableOptions'; -import styles from './ArtistIndexTable.css'; - -const bodyPadding = parseInt(dimensions.pageContentBodyPadding); -const bodyPaddingSmallScreen = parseInt( - dimensions.pageContentBodyPaddingSmallScreen -); - -interface RowItemData { - items: Artist[]; - sortKey: string; - columns: Column[]; - isSelectMode: boolean; -} - -interface ArtistIndexTableProps { - items: Artist[]; - sortKey: string; - sortDirection?: SortDirection; - jumpToCharacter?: string; - scrollTop?: number; - scrollerRef: RefObject; - isSelectMode: boolean; - isSmallScreen: boolean; -} - -const columnsSelector = createSelector( - (state: AppState) => state.artistIndex.columns, - (columns) => columns -); - -const Row: React.FC> = ({ - index, - style, - data, -}) => { - const { items, sortKey, columns, isSelectMode } = data; - - if (index >= items.length) { - return null; - } - - const artist = items[index]; - - return ( -
- -
- ); -}; - -function getWindowScrollTopPosition() { - return document.documentElement.scrollTop || document.body.scrollTop || 0; -} - -function ArtistIndexTable(props: ArtistIndexTableProps) { - const { - items, - sortKey, - sortDirection, - jumpToCharacter, - isSelectMode, - isSmallScreen, - scrollerRef, - } = props; - - const columns = useSelector(columnsSelector); - const { showBanners } = useSelector(selectTableOptions); - const listRef = useRef>(null); - const [measureRef, bounds] = useMeasure(); - const [size, setSize] = useState({ width: 0, height: 0 }); - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - - const rowHeight = useMemo(() => { - return showBanners ? 70 : 38; - }, [showBanners]); - - useEffect(() => { - const current = scrollerRef?.current as HTMLElement; - - if (isSmallScreen) { - setSize({ - width: windowWidth, - height: windowHeight, - }); - - return; - } - - if (current) { - const width = current.clientWidth; - const padding = - (isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5; - - setSize({ - width: width - padding * 2, - height: windowHeight, - }); - } - }, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]); - - useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; - - const handleScroll = throttle(() => { - const { offsetTop = 0 } = currentScrollerRef; - const scrollTop = - (isSmallScreen - ? getWindowScrollTopPosition() - : currentScrollerRef.scrollTop) - offsetTop; - - listRef.current?.scrollTo(scrollTop); - }, 10); - - currentScrollListener.addEventListener('scroll', handleScroll); - - return () => { - handleScroll.cancel(); - - if (currentScrollListener) { - currentScrollListener.removeEventListener('scroll', handleScroll); - } - }; - }, [isSmallScreen, listRef, scrollerRef]); - - useEffect(() => { - if (jumpToCharacter) { - const index = getIndexOfFirstCharacter(items, jumpToCharacter); - - if (index != null) { - let scrollTop = index * rowHeight; - - // If the offset is zero go to the top, otherwise offset - // by the approximate size of the header + padding (37 + 20). - if (scrollTop > 0) { - const offset = 57; - - scrollTop += offset; - } - - listRef.current?.scrollTo(scrollTop); - scrollerRef?.current?.scrollTo(0, scrollTop); - } - } - }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); - - return ( -
- - - - ref={listRef} - style={{ - width: '100%', - height: '100%', - overflow: 'none', - }} - width={size.width} - height={size.height} - itemCount={items.length} - itemSize={rowHeight} - itemData={{ - items, - sortKey, - columns, - isSelectMode, - }} - > - {Row} - - -
- ); -} - -export default ArtistIndexTable; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js new file mode 100644 index 000000000..3a97425cc --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js @@ -0,0 +1,29 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistSort } from 'Store/Actions/artistIndexActions'; +import ArtistIndexTable from './ArtistIndexTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.dimensions, + (state) => state.artistIndex.tableOptions, + (state) => state.artistIndex.columns, + (dimensions, tableOptions, columns) => { + return { + isSmallScreen: dimensions.isSmallScreen, + showBanners: tableOptions.showBanners, + columns + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onSortPress(sortKey) { + dispatch(setArtistSort({ sortKey })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexTable); diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx b/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx deleted file mode 100644 index 1b325c225..000000000 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableHeader.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import classNames from 'classnames'; -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { useSelect } from 'App/SelectContext'; -import ArtistIndexTableOptions from 'Artist/Index/Table/ArtistIndexTableOptions'; -import IconButton from 'Components/Link/IconButton'; -import Column from 'Components/Table/Column'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; -import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; -import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell'; -import { icons } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; -import { - setArtistSort, - setArtistTableOption, -} from 'Store/Actions/artistIndexActions'; -import { CheckInputChanged } from 'typings/inputs'; -import hasGrowableColumns from './hasGrowableColumns'; -import styles from './ArtistIndexTableHeader.css'; - -interface ArtistIndexTableHeaderProps { - showBanners: boolean; - columns: Column[]; - sortKey?: string; - sortDirection?: SortDirection; - isSelectMode: boolean; -} - -function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) { - const { showBanners, columns, sortKey, sortDirection, isSelectMode } = props; - const dispatch = useDispatch(); - const [selectState, selectDispatch] = useSelect(); - - const onSortPress = useCallback( - (value: string) => { - dispatch(setArtistSort({ sortKey: value })); - }, - [dispatch] - ); - - const onTableOptionChange = useCallback( - (payload: unknown) => { - dispatch(setArtistTableOption(payload)); - }, - [dispatch] - ); - - const onSelectAllChange = useCallback( - ({ value }: CheckInputChanged) => { - selectDispatch({ - type: value ? 'selectAll' : 'unselectAll', - }); - }, - [selectDispatch] - ); - - return ( - - {isSelectMode ? ( - - ) : null} - - {columns.map((column) => { - const { name, label, isSortable, isVisible } = column; - - if (!isVisible) { - return null; - } - - if (name === 'actions') { - return ( - - - - - - ); - } - - return ( - - {typeof label === 'function' ? label() : label} - - ); - })} - - ); -} - -export default ArtistIndexTableHeader; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.js b/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.js new file mode 100644 index 000000000..6fd619ad0 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.js @@ -0,0 +1,105 @@ +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import { inputTypes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +class ArtistIndexTableOptions extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + showBanners: props.showBanners, + showSearchAction: props.showSearchAction + }; + } + + componentDidUpdate(prevProps) { + const { + showBanners, + showSearchAction + } = this.props; + + if ( + showBanners !== prevProps.showBanners || + showSearchAction !== prevProps.showSearchAction + ) { + this.setState({ + showBanners, + showSearchAction + }); + } + } + + // + // Listeners + + onTableOptionChange = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onTableOptionChange({ + tableOptions: { + ...this.state, + [name]: value + } + }); + }); + }; + + // + // Render + + render() { + const { + showBanners, + showSearchAction + } = this.state; + + return ( + + + + {translate('ShowBanners')} + + + + + + + + {translate('ShowSearch')} + + + + + + ); + } +} + +ArtistIndexTableOptions.propTypes = { + showBanners: PropTypes.bool.isRequired, + showSearchAction: PropTypes.bool.isRequired, + onTableOptionChange: PropTypes.func.isRequired +}; + +export default ArtistIndexTableOptions; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.tsx b/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.tsx deleted file mode 100644 index 9c10d859d..000000000 --- a/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Fragment, useCallback } from 'react'; -import { useSelector } from 'react-redux'; -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'; - -interface ArtistIndexTableOptionsProps { - onTableOptionChange(...args: unknown[]): unknown; -} - -function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) { - const { onTableOptionChange } = props; - - const tableOptions = useSelector(selectTableOptions); - - const { showBanners, showSearchAction } = tableOptions; - - const onTableOptionChangeWrapper = useCallback( - ({ name, value }: CheckInputChanged) => { - onTableOptionChange({ - tableOptions: { - ...tableOptions, - [name]: value, - }, - }); - }, - [tableOptions, onTableOptionChange] - ); - - return ( - - - {translate('ShowBanners')} - - - - - - {translate('ShowSearch')} - - - - - ); -} - -export default ArtistIndexTableOptions; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableOptionsConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexTableOptionsConnector.js new file mode 100644 index 000000000..0a1607cf2 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableOptionsConnector.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import ArtistIndexTableOptions from './ArtistIndexTableOptions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex.tableOptions, + (tableOptions) => { + return tableOptions; + } + ); +} + +export default connect(createMapStateToProps)(ArtistIndexTableOptions); diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.js b/frontend/src/Artist/Index/Table/ArtistStatusCell.js new file mode 100644 index 000000000..1f163a473 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistStatusCell.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './ArtistStatusCell.css'; + +function ArtistStatusCell(props) { + const { + className, + artistType, + monitored, + status, + isSaving, + onMonitoredPress, + component: Component, + ...otherProps + } = props; + + const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive'; + + return ( + + + + + + ); +} + +ArtistStatusCell.propTypes = { + className: PropTypes.string.isRequired, + artistType: PropTypes.string, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + isSaving: PropTypes.bool.isRequired, + onMonitoredPress: PropTypes.func.isRequired, + component: PropTypes.elementType +}; + +ArtistStatusCell.defaultProps = { + className: styles.status, + component: VirtualTableRowCell +}; + +export default ArtistStatusCell; diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.tsx b/frontend/src/Artist/Index/Table/ArtistStatusCell.tsx deleted file mode 100644 index 00c7ae4c8..000000000 --- a/frontend/src/Artist/Index/Table/ArtistStatusCell.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import Icon from 'Components/Icon'; -import MonitorToggleButton from 'Components/MonitorToggleButton'; -import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; -import { icons } from 'Helpers/Props'; -import { toggleArtistMonitored } from 'Store/Actions/artistActions'; -import translate from 'Utilities/String/translate'; -import styles from './ArtistStatusCell.css'; - -interface ArtistStatusCellProps { - className: string; - artistId: number; - artistType?: string; - monitored: boolean; - status: string; - isSelectMode: boolean; - isSaving: boolean; - component?: React.ElementType; -} - -function ArtistStatusCell(props: ArtistStatusCellProps) { - const { - className, - artistId, - artistType, - monitored, - status, - isSelectMode, - isSaving, - component: Component = VirtualTableRowCell, - ...otherProps - } = props; - - const endedString = - artistType === 'Person' ? translate('Deceased') : translate('Inactive'); - const dispatch = useDispatch(); - - const onMonitoredPress = useCallback(() => { - dispatch(toggleArtistMonitored({ artistId, monitored: !monitored })); - }, [artistId, monitored, dispatch]); - - return ( - - {isSelectMode ? ( - - ) : ( - - )} - - - - ); -} - -export default ArtistStatusCell; diff --git a/frontend/src/Artist/Index/Table/hasGrowableColumns.js b/frontend/src/Artist/Index/Table/hasGrowableColumns.js new file mode 100644 index 000000000..994436d9f --- /dev/null +++ b/frontend/src/Artist/Index/Table/hasGrowableColumns.js @@ -0,0 +1,16 @@ +const growableColumns = [ + 'qualityProfileId', + 'path', + 'tags' +]; + +export default function hasGrowableColumns(columns) { + return columns.some((column) => { + const { + name, + isVisible + } = column; + + return growableColumns.includes(name) && isVisible; + }); +} diff --git a/frontend/src/Artist/Index/Table/hasGrowableColumns.ts b/frontend/src/Artist/Index/Table/hasGrowableColumns.ts deleted file mode 100644 index ed0cc6c58..000000000 --- a/frontend/src/Artist/Index/Table/hasGrowableColumns.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Column from 'Components/Table/Column'; - -const growableColumns = ['qualityProfileId', 'path', 'tags']; - -export default function hasGrowableColumns(columns: Column[]) { - return columns.some((column) => { - const { name, isVisible } = column; - - return growableColumns.includes(name) && isVisible; - }); -} diff --git a/frontend/src/Artist/Index/Table/selectTableOptions.ts b/frontend/src/Artist/Index/Table/selectTableOptions.ts deleted file mode 100644 index b6a2a6a94..000000000 --- a/frontend/src/Artist/Index/Table/selectTableOptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -const selectTableOptions = createSelector( - (state: AppState) => state.artistIndex.tableOptions, - (tableOptions) => tableOptions -); - -export default selectTableOptions; diff --git a/frontend/src/Artist/Index/createArtistIndexItemSelector.ts b/frontend/src/Artist/Index/createArtistIndexItemSelector.ts deleted file mode 100644 index 4388a3aeb..000000000 --- a/frontend/src/Artist/Index/createArtistIndexItemSelector.ts +++ /dev/null @@ -1,45 +0,0 @@ -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'; -import { createArtistSelectorForHook } from 'Store/Selectors/createArtistSelector'; -import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; - -function createArtistIndexItemSelector(artistId: number) { - return createSelector( - createArtistSelectorForHook(artistId), - createArtistQualityProfileSelector(artistId), - createArtistMetadataProfileSelector(artistId), - createExecutingCommandsSelector(), - ( - artist: Artist, - qualityProfile, - metadataProfile, - executingCommands: Command[] - ) => { - const isRefreshingArtist = executingCommands.some((command) => { - return ( - command.name === REFRESH_ARTIST && command.body.artistId === artistId - ); - }); - - const isSearchingArtist = executingCommands.some((command) => { - return ( - command.name === ARTIST_SEARCH && command.body.artistId === artistId - ); - }); - - return { - artist, - qualityProfile, - metadataProfile, - isRefreshingArtist, - isSearchingArtist, - }; - } - ); -} - -export default createArtistIndexItemSelector; diff --git a/frontend/src/Artist/Index/createArtistQueueDetailsSelector.ts b/frontend/src/Artist/Index/createArtistQueueDetailsSelector.ts deleted file mode 100644 index 34a9c3910..000000000 --- a/frontend/src/Artist/Index/createArtistQueueDetailsSelector.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -export interface ArtistQueueDetails { - count: number; - tracksWithFiles: number; -} - -function createArtistQueueDetailsSelector(artistId: number) { - return createSelector( - (state: AppState) => state.queue.details.items, - (queueItems) => { - return queueItems.reduce( - (acc: ArtistQueueDetails, item) => { - if ( - item.trackedDownloadState === 'imported' || - item.artistId !== artistId - ) { - return acc; - } - - acc.count += item.trackFileCount ?? 0; - - if (item.trackHasFileCount) { - acc.tracksWithFiles += item.trackHasFileCount; - } - - return acc; - }, - { - count: 0, - tracksWithFiles: 0, - } - ); - } - ); -} - -export default createArtistQueueDetailsSelector; diff --git a/frontend/src/Artist/MonitoringOptions/MonitoringOptionModalConnector.js b/frontend/src/Artist/MonitoringOptions/MonitoringOptionModalConnector.js deleted file mode 100644 index 6ea9e8290..000000000 --- a/frontend/src/Artist/MonitoringOptions/MonitoringOptionModalConnector.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import MonitoringOptionsModal from './MonitoringOptionsModal'; - -const mapDispatchToProps = { - clearPendingChanges -}; - -class MonitoringOptionsModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'artist' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -MonitoringOptionsModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(undefined, mapDispatchToProps)(MonitoringOptionsModalConnector); diff --git a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css deleted file mode 100644 index 3e9e3ffd0..000000000 --- a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css +++ /dev/null @@ -1,9 +0,0 @@ -.labelIcon { - margin-left: 8px; -} - -.message { - composes: alert from '~Components/Alert.css'; - - margin-bottom: 30px; -} diff --git a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css.d.ts b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css.d.ts deleted file mode 100644 index af0f6cd46..000000000 --- a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'labelIcon': string; - 'message': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js index b1550d39e..230b22f2a 100644 --- a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js +++ b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js @@ -1,22 +1,18 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent'; import Alert from 'Components/Alert'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; -import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; import SpinnerButton from 'Components/Link/SpinnerButton'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import Popover from 'Components/Tooltip/Popover'; -import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; +import { inputTypes, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; -import styles from './MonitoringOptionsModalContent.css'; const NO_CHANGE = 'noChange'; @@ -55,7 +51,8 @@ class MonitoringOptionsModalContent extends Component { onSavePress = () => { const { - onSavePress + onSavePress, + isSaving } = this.props; const { monitor @@ -64,6 +61,14 @@ class MonitoringOptionsModalContent extends Component { if (monitor !== NO_CHANGE) { onSavePress({ monitor }); } + + if (!isSaving) { + this.onModalClose(); + } + }; + + onModalClose = () => { + this.props.onModalClose(); }; // @@ -84,31 +89,19 @@ class MonitoringOptionsModalContent extends Component { return ( - {translate('MonitorArtist')} + {translate('MonitorAlbum')} - - {translate('MonitorAlbumExistingOnlyWarning')} + +
+ {translate('MonitorAlbumExistingOnlyWarning')} +
- - {translate('MonitorExistingAlbums')} - - - } - title={translate('MonitoringOptions')} - body={} - position={tooltipPositions.RIGHT} - /> - + {translate('Monitoring')} state.artist, - (artistState) => { - const { - isSaving, - saveError - } = artistState; - - return { - isSaving, - saveError - }; - } - ); -} - -const mapDispatchToProps = { - dispatchUpdateMonitoringOptions: updateArtistsMonitor -}; - -class MonitoringOptionsModalContentConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(true); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.setState({ name, value }); - }; - - onSavePress = ({ monitor }) => { - this.props.dispatchUpdateMonitoringOptions({ - artistIds: [this.props.artistId], - monitor, - shouldFetchAlbumsAfterUpdate: true - }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -MonitoringOptionsModalContentConnector.propTypes = { - artistId: PropTypes.number.isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - dispatchUpdateMonitoringOptions: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(MonitoringOptionsModalContentConnector); diff --git a/frontend/src/Artist/MoveArtist/MoveArtistModal.js b/frontend/src/Artist/MoveArtist/MoveArtistModal.js index 1bad51151..9d6e54c81 100644 --- a/frontend/src/Artist/MoveArtist/MoveArtistModal.js +++ b/frontend/src/Artist/MoveArtist/MoveArtistModal.js @@ -15,7 +15,6 @@ function MoveArtistModal(props) { destinationPath, destinationRootFolder, isOpen, - onModalClose, onSavePress, onMoveArtistPress } = props; @@ -34,11 +33,11 @@ function MoveArtistModal(props) { isOpen={isOpen} size={sizes.MEDIUM} closeOnBackgroundClick={false} - onModalClose={onModalClose} + onModalClose={onSavePress} > Move Files @@ -77,7 +76,6 @@ MoveArtistModal.propTypes = { destinationPath: PropTypes.string, destinationRootFolder: PropTypes.string, isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired, onMoveArtistPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js index 9ce7e8f9a..d7feee98d 100644 --- a/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js +++ b/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js @@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) { return ( diff --git a/frontend/src/Calendar/CalendarConnector.js b/frontend/src/Calendar/CalendarConnector.js index 17221eb34..079b3e780 100644 --- a/frontend/src/Calendar/CalendarConnector.js +++ b/frontend/src/Calendar/CalendarConnector.js @@ -47,7 +47,7 @@ class CalendarConnector extends Component { gotoCalendarToday } = this.props; - registerPagePopulator(this.repopulate, ['trackFileUpdated', 'trackFileDeleted']); + registerPagePopulator(this.repopulate); if (useCurrentPage) { fetchCalendar(); diff --git a/frontend/src/Calendar/CalendarFilterModal.tsx b/frontend/src/Calendar/CalendarFilterModal.tsx deleted file mode 100644 index e26b2928b..000000000 --- a/frontend/src/Calendar/CalendarFilterModal.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js index bf7f46c10..3a4603e82 100644 --- a/frontend/src/Calendar/CalendarPage.js +++ b/frontend/src/Calendar/CalendarPage.js @@ -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} /> @@ -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, diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js index 4221c0339..d0e7e87af 100644 --- a/frontend/src/Calendar/CalendarPageConnector.js +++ b/frontend/src/Calendar/CalendarPageConnector.js @@ -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, diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js index 3473f4c31..844ffec5f 100644 --- a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js @@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component { type={inputTypes.TAG} name="tags" value={tags} - helpText={translate('ICalTagsArtistHelpText')} + helpText={translate('TagsHelpText')} onChange={this.onInputChange} /> diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts deleted file mode 100644 index 09a03865d..000000000 --- a/frontend/src/Commands/Command.ts +++ /dev/null @@ -1,38 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export interface CommandBody { - sendUpdatesToClient: boolean; - updateScheduledTask: boolean; - completionMessage: string; - requiresDiskAccess: boolean; - isExclusive: boolean; - isLongRunning: boolean; - name: string; - lastExecutionTime: string; - lastStartTime: string; - trigger: string; - suppressMessages: boolean; - artistId?: number; - artistIds?: number[]; -} - -interface Command extends ModelBase { - name: string; - commandName: string; - message: string; - body: CommandBody; - priority: string; - status: string; - result: string; - queued: string; - started: string; - ended: string; - duration: string; - trigger: string; - stateChangeTime: string; - sendUpdatesToClient: boolean; - updateScheduledTask: boolean; - lastExecutionTime: string; -} - -export default Command; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css index 786123fb7..b23415a76 100644 --- a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css @@ -1,7 +1,9 @@ +.description { + line-height: $lineHeight; +} + .description { margin-left: 0; - line-height: $lineHeight; - overflow-wrap: break-word; } @media (min-width: 768px) { diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js index dff1fbf6e..0e4d6a015 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import Alert from 'Components/Alert'; import PathInput from 'Components/Form/PathInput'; import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; @@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component { className={styles.mappedDrivesWarning} kind={kinds.WARNING} > - + Mapped network drives are not available when running as a Windows Service, see the FAQ for more information. } diff --git a/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx deleted file mode 100644 index 486027f35..000000000 --- a/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import Artist from 'Artist/Artist'; -import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; -import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; - -function ArtistFilterBuilderRowValue(props: FilterBuilderRowValueProps) { - const allArtists: Artist[] = useSelector(createAllArtistSelector()); - - const tagList = allArtists - .map((artist) => ({ id: artist.id, name: artist.artistName })) - .sort(sortByProp('name')); - - return ; -} - -export default ArtistFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js index 0c4a31657..d33f4d4fb 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js @@ -1,4 +1,3 @@ -import { maxBy } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -51,7 +50,7 @@ class FilterBuilderModalContent extends Component { if (id) { dispatchSetFilter({ selectedFilterKey: id }); } else { - const last = maxBy(customFilters, 'id'); + const last = customFilters[customFilters.length -1]; dispatchSetFilter({ selectedFilterKey: last.id }); } @@ -109,7 +108,7 @@ class FilterBuilderModalContent extends Component { this.setState({ labelErrors: [ { - message: translate('LabelIsRequired') + message: 'Label is required' } ] }); @@ -147,7 +146,7 @@ class FilterBuilderModalContent extends Component { return ( - {translate('CustomFilter')} + Custom Filter @@ -167,9 +166,7 @@ class FilterBuilderModalContent extends Component {
-
- {translate('Filters')} -
+
{translate('Filters')}
{ diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 77dad7173..0560b76b0 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,19 +3,15 @@ import React, { Component } from 'react'; import SelectInput from 'Components/Form/SelectInput'; import IconButton from 'Components/Link/IconButton'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; -import ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue'; import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; -import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; -import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue'; -import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue'; +import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; -import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; +import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import styles from './FilterBuilderRow.css'; @@ -61,17 +57,11 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.DATE: return DateFilterBuilderRowValue; - case filterBuilderValueTypes.HISTORY_EVENT_TYPE: - return HistoryEventTypeFilterBuilderRowValue; - case filterBuilderValueTypes.INDEXER: return IndexerFilterBuilderRowValueConnector; case filterBuilderValueTypes.METADATA_PROFILE: - return MetadataProfileFilterBuilderRowValue; - - case filterBuilderValueTypes.MONITOR_NEW_ITEMS: - return MonitorNewItemsFilterBuilderRowValue; + return MetadataProfileFilterBuilderRowValueConnector; case filterBuilderValueTypes.PROTOCOL: return ProtocolFilterBuilderRowValue; @@ -80,10 +70,7 @@ function getRowValueConnector(selectedFilterBuilderProp) { return QualityFilterBuilderRowValueConnector; case filterBuilderValueTypes.QUALITY_PROFILE: - return QualityProfileFilterBuilderRowValue; - - case filterBuilderValueTypes.ARTIST: - return ArtistFilterBuilderRowValue; + return QualityProfileFilterBuilderRowValueConnector; case filterBuilderValueTypes.ARTIST_STATUS: return ArtistStatusFilterBuilderRowValue; @@ -225,7 +212,7 @@ class FilterBuilderRow extends Component { key: name, value: typeof label === 'function' ? label() : label }; - }).sort(sortByProp('value')); + }).sort((a, b) => a.value.localeCompare(b.value)); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js index d1419327a..a7aed80b6 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { filterBuilderTypes } from 'Helpers/Props'; import * as filterTypes from 'Helpers/Props/filterTypes'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { @@ -38,7 +38,7 @@ function createTagListSelector() { } return acc; - }, []).sort(sortByProp('name')); + }, []).sort(sortByName); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts deleted file mode 100644 index 5bf9e5785..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FilterBuilderProp } from 'App/State/AppState'; - -interface FilterBuilderRowOnChangeProps { - name: string; - value: unknown[]; -} - -interface FilterBuilderRowValueProps { - filterType?: string; - filterValue: string | number | object | string[] | number[] | object[]; - selectedFilterBuilderProp: FilterBuilderProp; - sectionItem: unknown[]; - onChange: (payload: FilterBuilderRowOnChangeProps) => void; -} - -export default FilterBuilderRowValueProps; diff --git a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx deleted file mode 100644 index 1b3b369be..000000000 --- a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import translate from 'Utilities/String/translate'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; -import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; - -const EVENT_TYPE_OPTIONS = [ - { - id: 1, - get name() { - return translate('Grabbed'); - }, - }, - { - id: 3, - get name() { - return translate('TrackImported'); - }, - }, - { - id: 4, - get name() { - return translate('DownloadFailed'); - }, - }, - { - id: 7, - get name() { - return translate('ImportCompleteFailed'); - }, - }, - { - id: 8, - get name() { - return translate('DownloadImported'); - }, - }, - { - id: 5, - get name() { - return translate('Deleted'); - }, - }, - { - id: 6, - get name() { - return translate('Renamed'); - }, - }, - { - id: 9, - get name() { - return translate('Retagged'); - }, - }, - { - id: 7, - get name() { - return translate('Ignored'); - }, - }, -]; - -function HistoryEventTypeFilterBuilderRowValue( - props: FilterBuilderRowValueProps -) { - return ; -} - -export default HistoryEventTypeFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValue.tsx deleted file mode 100644 index bbd9a8274..000000000 --- a/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValue.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; -import sortByProp from 'Utilities/Array/sortByProp'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; - -function createMetadataProfilesSelector() { - return createSelector( - (state: AppState) => state.settings.metadataProfiles.items, - (metadataProfiles) => { - return metadataProfiles; - } - ); -} - -function MetadataProfileFilterBuilderRowValue( - props: FilterBuilderRowValueProps -) { - const metadataProfiles = useSelector(createMetadataProfilesSelector()); - - const tagList = metadataProfiles - .map(({ id, name }) => ({ id, name })) - .sort(sortByProp('name')); - - return ; -} - -export default MetadataProfileFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..89d6c06b3 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.metadataProfiles, + (metadataProfiles) => { + const tagList = metadataProfiles.items.map((metadataProfile) => { + const { + id, + name + } = metadataProfile; + + return { + id, + name + }; + }); + + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Filter/Builder/MonitorNewItemsFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/MonitorNewItemsFilterBuilderRowValue.tsx deleted file mode 100644 index 812d8c5b1..000000000 --- a/frontend/src/Components/Filter/Builder/MonitorNewItemsFilterBuilderRowValue.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; -import translate from 'Utilities/String/translate'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; - -const options = [ - { - id: 'all', - get name() { - return translate('AllAlbums'); - }, - }, - { - id: 'new', - get name() { - return translate('New'); - }, - }, - { - id: 'none', - get name() { - return translate('None'); - }, - }, -]; - -function MonitorNewItemsFilterBuilderRowValue( - props: FilterBuilderRowValueProps -) { - return ; -} - -export default MonitorNewItemsFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx deleted file mode 100644 index 50036cb90..000000000 --- a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; -import sortByProp from 'Utilities/Array/sortByProp'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; - -function createQualityProfilesSelector() { - return createSelector( - (state: AppState) => state.settings.qualityProfiles.items, - (qualityProfiles) => { - return qualityProfiles; - } - ); -} - -function QualityProfileFilterBuilderRowValue( - props: FilterBuilderRowValueProps -) { - const qualityProfiles = useSelector(createQualityProfilesSelector()); - - const tagList = qualityProfiles - .map(({ id, name }) => ({ id, name })) - .sort(sortByProp('name')); - - return ; -} - -export default QualityProfileFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..4a8b82283 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityProfiles, + (qualityProfiles) => { + const tagList = qualityProfiles.items.map((qualityProfile) => { + const { + id, + name + } = qualityProfile; + + return { + id, + name + }; + }); + + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js index 9f378d5a2..7407f729a 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js @@ -37,8 +37,8 @@ class CustomFilter extends Component { dispatchSetFilter } = this.props; - // Assume that delete and then unmounting means the deletion was successful. - // Moving this check to an ancestor would be more accurate, but would have + // Assume that delete and then unmounting means the delete was successful. + // Moving this check to a ancestor would be more accurate, but would have // more boilerplate. if (this.state.isDeleting && id === selectedFilterKey) { dispatchSetFilter({ selectedFilterKey: 'all' }); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index d70b97e44..116bd3e8b 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -5,7 +5,6 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import CustomFilter from './CustomFilter'; import styles from './CustomFiltersModalContent.css'; @@ -31,24 +30,22 @@ function CustomFiltersModalContent(props) { { - customFilters - .sort((a, b) => sortByProp(a, b, 'label')) - .map((customFilter) => { - return ( - - ); - }) + customFilters.map((customFilter) => { + return ( + + ); + }) }
diff --git a/frontend/src/Components/Form/ArtistTagInput.tsx b/frontend/src/Components/Form/ArtistTagInput.tsx deleted file mode 100644 index 3edb46ec4..000000000 --- a/frontend/src/Components/Form/ArtistTagInput.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useCallback } from 'react'; -import TagInputConnector from './TagInputConnector'; - -interface ArtistTagInputProps { - name: string; - value: number | number[]; - onChange: ({ - name, - value, - }: { - name: string; - value: number | number[]; - }) => void; -} - -export default function ArtistTagInput(props: ArtistTagInputProps) { - const { value, onChange, ...otherProps } = props; - const isArray = Array.isArray(value); - - const handleChange = useCallback( - ({ name, value: newValue }: { name: string; value: number[] }) => { - if (isArray) { - onChange({ name, value: newValue }); - } else { - onChange({ - name, - value: newValue.length ? newValue[newValue.length - 1] : 0, - }); - } - }, - [isArray, onChange] - ); - - let finalValue: number[] = []; - - if (isArray) { - finalValue = value; - } else if (value === 0) { - finalValue = []; - } else { - finalValue = [value]; - } - - return ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore 2786 'TagInputConnector' isn't typed yet - - ); -} diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index c21f0ded6..c89016869 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -4,8 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; +import sortByName from 'Utilities/Array/sortByName'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -23,18 +22,17 @@ function createMapStateToProps() { const filteredItems = items.filter((item) => item.protocol === protocolFilter); - const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => { + const values = _.map(filteredItems.sort(sortByName), (downloadClient) => { return { key: downloadClient.id, - value: downloadClient.name, - hint: `(${downloadClient.id})` + value: downloadClient.name }; }); if (includeAny) { values.unshift({ key: 0, - value: `(${translate('Any')})` + value: '(Any)' }); } diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css index defefb18e..56f5564b9 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -19,7 +19,7 @@ .isDisabled { opacity: 0.7; - cursor: not-allowed !important; + cursor: not-allowed; } .dropdownArrowContainer { diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index 8327b9385..cc4215025 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import TextInput from './TextInput'; import styles from './EnhancedSelectInput.css'; -const MINIMUM_DISTANCE_FROM_EDGE = 10; - function isArrowKey(keyCode) { return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; } @@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component { // Listeners onComputeMaxHeight = (data) => { + const { + top, + bottom + } = data.offsets.reference; + const windowHeight = window.innerHeight; - data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; + if ((/^botton/).test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } return data; }; @@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component { order: 851, enabled: true, fn: this.onComputeMaxHeight - }, - preventOverflow: { - enabled: true, - boundariesElement: 'viewport' } }} > diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 3173b493d..7e6b0c7c9 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -4,7 +4,6 @@ import Link from 'Components/Link/Link'; import { inputTypes, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector'; -import ArtistTagInput from './ArtistTagInput'; import AutoCompleteInput from './AutoCompleteInput'; import CaptchaInputConnector from './CaptchaInputConnector'; import CheckInput from './CheckInput'; @@ -13,7 +12,6 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import FormInputHelpText from './FormInputHelpText'; -import IndexerFlagsSelectInput from './IndexerFlagsSelectInput'; import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import KeyValueListInput from './KeyValueListInput'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; @@ -49,12 +47,12 @@ function getComponent(type) { case inputTypes.DEVICE: return DeviceInputConnector; - case inputTypes.KEY_VALUE_LIST: - return KeyValueListInput; - case inputTypes.PLAYLIST: return PlaylistInputConnector; + case inputTypes.KEY_VALUE_LIST: + return KeyValueListInput; + case inputTypes.MONITOR_ALBUMS_SELECT: return MonitorAlbumsSelectInput; @@ -85,9 +83,6 @@ function getComponent(type) { case inputTypes.INDEXER_SELECT: return IndexerSelectInputConnector; - case inputTypes.INDEXER_FLAGS_SELECT: - return IndexerFlagsSelectInput; - case inputTypes.DOWNLOAD_CLIENT_SELECT: return DownloadClientSelectInputConnector; @@ -100,9 +95,6 @@ function getComponent(type) { case inputTypes.DYNAMIC_SELECT: return EnhancedSelectInputConnector; - case inputTypes.ARTIST_TAG: - return ArtistTagInput; - case inputTypes.SERIES_TYPE_SELECT: return SeriesTypeSelectInput; @@ -284,7 +276,6 @@ FormInputGroup.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.any, values: PropTypes.arrayOf(PropTypes.any), - isDisabled: PropTypes.bool, type: PropTypes.string.isRequired, kind: PropTypes.oneOf(kinds.all), min: PropTypes.number, @@ -298,9 +289,7 @@ FormInputGroup.propTypes = { autoFocus: PropTypes.bool, includeNoChange: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool, - includeNone: PropTypes.bool, selectedValueOptions: PropTypes.object, - indexerFlags: PropTypes.number, pending: PropTypes.bool, errors: PropTypes.arrayOf(PropTypes.object), warnings: PropTypes.arrayOf(PropTypes.object), diff --git a/frontend/src/Components/Form/FormLabel.css b/frontend/src/Components/Form/FormLabel.css index 54a4678e8..074b6091d 100644 --- a/frontend/src/Components/Form/FormLabel.css +++ b/frontend/src/Components/Form/FormLabel.css @@ -2,10 +2,8 @@ display: flex; justify-content: flex-end; margin-right: $formLabelRightMarginWidth; - padding-top: 8px; - min-height: 35px; - text-align: end; font-weight: bold; + line-height: 35px; } .hasError { diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx deleted file mode 100644 index 8dbd27a70..000000000 --- a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -const selectIndexerFlagsValues = (selectedFlags: number) => - createSelector( - (state: AppState) => state.settings.indexerFlags, - (indexerFlags) => { - const value = indexerFlags.items.reduce((acc: number[], { id }) => { - // eslint-disable-next-line no-bitwise - if ((selectedFlags & id) === id) { - acc.push(id); - } - - return acc; - }, []); - - const values = indexerFlags.items.map(({ id, name }) => ({ - key: id, - value: name, - })); - - return { - value, - values, - }; - } - ); - -interface IndexerFlagsSelectInputProps { - name: string; - indexerFlags: number; - onChange(payload: object): void; -} - -function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) { - const { indexerFlags, onChange } = props; - - const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags)); - - const onChangeWrapper = useCallback( - ({ name, value }: { name: string; value: number[] }) => { - const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0); - - onChange({ name, value: indexerFlags }); - }, - [onChange] - ); - - return ( - - ); -} - -export default IndexerFlagsSelectInput; diff --git a/frontend/src/Components/Form/IndexerSelectInputConnector.js b/frontend/src/Components/Form/IndexerSelectInputConnector.js index 5f62becbb..cd58270eb 100644 --- a/frontend/src/Components/Form/IndexerSelectInputConnector.js +++ b/frontend/src/Components/Form/IndexerSelectInputConnector.js @@ -4,8 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchIndexers } from 'Store/Actions/settingsActions'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; +import sortByName from 'Utilities/Array/sortByName'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -20,7 +19,7 @@ function createMapStateToProps() { items } = indexers; - const values = _.map(items.sort(sortByProp('name')), (indexer) => { + const values = _.map(items.sort(sortByName), (indexer) => { return { key: indexer.id, value: indexer.name @@ -30,7 +29,7 @@ function createMapStateToProps() { if (includeAny) { values.unshift({ key: 0, - value: `(${translate('Any')})` + value: '(Any)' }); } diff --git a/frontend/src/Components/Form/KeyValueListInput.js b/frontend/src/Components/Form/KeyValueListInput.js new file mode 100644 index 000000000..3e73d74f3 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInput.js @@ -0,0 +1,156 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import KeyValueListInputItem from './KeyValueListInputItem'; +import styles from './KeyValueListInput.css'; + +class KeyValueListInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isFocused: false + }; + } + + // + // Listeners + + onItemChange = (index, itemValue) => { + const { + name, + value, + onChange + } = this.props; + + const newValue = [...value]; + + if (index == null) { + newValue.push(itemValue); + } else { + newValue.splice(index, 1, itemValue); + } + + onChange({ + name, + value: newValue + }); + }; + + onRemoveItem = (index) => { + const { + name, + value, + onChange + } = this.props; + + const newValue = [...value]; + newValue.splice(index, 1); + + onChange({ + name, + value: newValue + }); + }; + + onFocus = () => { + this.setState({ + isFocused: true + }); + }; + + onBlur = () => { + this.setState({ + isFocused: false + }); + + const { + name, + value, + onChange + } = this.props; + + const newValue = value.reduce((acc, v) => { + if (v.key || v.value) { + acc.push(v); + } + + return acc; + }, []); + + if (newValue.length !== value.length) { + onChange({ + name, + value: newValue + }); + } + }; + + // + // Render + + render() { + const { + className, + value, + keyPlaceholder, + valuePlaceholder, + hasError, + hasWarning + } = this.props; + + const { isFocused } = this.state; + + return ( +
+ { + [...value, { key: '', value: '' }].map((v, index) => { + return ( + + ); + }) + } +
+ ); + } +} + +KeyValueListInput.propTypes = { + className: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.arrayOf(PropTypes.object).isRequired, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + keyPlaceholder: PropTypes.string, + valuePlaceholder: PropTypes.string, + onChange: PropTypes.func.isRequired +}; + +KeyValueListInput.defaultProps = { + className: styles.inputContainer, + value: [] +}; + +export default KeyValueListInput; diff --git a/frontend/src/Components/Form/KeyValueListInput.tsx b/frontend/src/Components/Form/KeyValueListInput.tsx deleted file mode 100644 index f5c6ac19b..000000000 --- a/frontend/src/Components/Form/KeyValueListInput.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import classNames from 'classnames'; -import React, { useCallback, useState } from 'react'; -import { InputOnChange } from 'typings/inputs'; -import KeyValueListInputItem from './KeyValueListInputItem'; -import styles from './KeyValueListInput.css'; - -interface KeyValue { - key: string; - value: string; -} - -export interface KeyValueListInputProps { - className?: string; - name: string; - value: KeyValue[]; - hasError?: boolean; - hasWarning?: boolean; - keyPlaceholder?: string; - valuePlaceholder?: string; - onChange: InputOnChange; -} - -function KeyValueListInput({ - className = styles.inputContainer, - name, - value = [], - hasError = false, - hasWarning = false, - keyPlaceholder, - valuePlaceholder, - onChange, -}: KeyValueListInputProps): JSX.Element { - const [isFocused, setIsFocused] = useState(false); - - const handleItemChange = useCallback( - (index: number | null, itemValue: KeyValue) => { - const newValue = [...value]; - - if (index === null) { - newValue.push(itemValue); - } else { - newValue.splice(index, 1, itemValue); - } - - onChange({ name, value: newValue }); - }, - [value, name, onChange] - ); - - const handleRemoveItem = useCallback( - (index: number) => { - const newValue = [...value]; - newValue.splice(index, 1); - onChange({ name, value: newValue }); - }, - [value, name, onChange] - ); - - const onFocus = useCallback(() => setIsFocused(true), []); - - const onBlur = useCallback(() => { - setIsFocused(false); - - const newValue = value.reduce((acc: KeyValue[], v) => { - if (v.key || v.value) { - acc.push(v); - } - return acc; - }, []); - - if (newValue.length !== value.length) { - onChange({ name, value: newValue }); - } - }, [value, name, onChange]); - - return ( -
- {[...value, { key: '', value: '' }].map((v, index) => ( - - ))} -
- ); -} - -export default KeyValueListInput; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css b/frontend/src/Components/Form/KeyValueListInputItem.css index ed82db459..dca2882a8 100644 --- a/frontend/src/Components/Form/KeyValueListInputItem.css +++ b/frontend/src/Components/Form/KeyValueListInputItem.css @@ -5,19 +5,13 @@ &:last-child { margin-bottom: 0; - border-bottom: 0; } } -.keyInputWrapper { +.inputWrapper { flex: 1 0 0; } -.valueInputWrapper { - flex: 1 0 0; - min-width: 40px; -} - .buttonWrapper { flex: 0 0 22px; } @@ -26,10 +20,6 @@ .valueInput { width: 100%; border: none; - background-color: transparent; + background-color: var(--inputBackgroundColor); color: var(--textColor); - - &::placeholder { - color: var(--helpTextColor); - } } diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts index aa0c1be13..35baf55cd 100644 --- a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts +++ b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts @@ -2,11 +2,10 @@ // Please do not change this file! interface CssExports { 'buttonWrapper': string; + 'inputWrapper': string; 'itemContainer': string; 'keyInput': string; - 'keyInputWrapper': string; 'valueInput': string; - 'valueInputWrapper': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.js b/frontend/src/Components/Form/KeyValueListInputItem.js new file mode 100644 index 000000000..5379c2129 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInputItem.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import IconButton from 'Components/Link/IconButton'; +import { icons } from 'Helpers/Props'; +import TextInput from './TextInput'; +import styles from './KeyValueListInputItem.css'; + +class KeyValueListInputItem extends Component { + + // + // Listeners + + onKeyChange = ({ value: keyValue }) => { + const { + index, + value, + onChange + } = this.props; + + onChange(index, { key: keyValue, value }); + }; + + onValueChange = ({ value }) => { + // TODO: Validate here or validate at a lower level component + + const { + index, + keyValue, + onChange + } = this.props; + + onChange(index, { key: keyValue, value }); + }; + + onRemovePress = () => { + const { + index, + onRemove + } = this.props; + + onRemove(index); + }; + + onFocus = () => { + this.props.onFocus(); + }; + + onBlur = () => { + this.props.onBlur(); + }; + + // + // Render + + render() { + const { + keyValue, + value, + keyPlaceholder, + valuePlaceholder, + isNew + } = this.props; + + return ( +
+
+ +
+ +
+ +
+ +
+ { + isNew ? + null : + + } +
+
+ ); + } +} + +KeyValueListInputItem.propTypes = { + index: PropTypes.number, + keyValue: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + keyPlaceholder: PropTypes.string.isRequired, + valuePlaceholder: PropTypes.string.isRequired, + isNew: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + onFocus: PropTypes.func.isRequired, + onBlur: PropTypes.func.isRequired +}; + +KeyValueListInputItem.defaultProps = { + keyPlaceholder: 'Key', + valuePlaceholder: 'Value' +}; + +export default KeyValueListInputItem; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.tsx b/frontend/src/Components/Form/KeyValueListInputItem.tsx deleted file mode 100644 index c63ad50a9..000000000 --- a/frontend/src/Components/Form/KeyValueListInputItem.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useCallback } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import { icons } from 'Helpers/Props'; -import TextInput from './TextInput'; -import styles from './KeyValueListInputItem.css'; - -interface KeyValueListInputItemProps { - index: number; - keyValue: string; - value: string; - keyPlaceholder?: string; - valuePlaceholder?: string; - isNew: boolean; - onChange: (index: number, itemValue: { key: string; value: string }) => void; - onRemove: (index: number) => void; - onFocus: () => void; - onBlur: () => void; -} - -function KeyValueListInputItem({ - index, - keyValue, - value, - keyPlaceholder = 'Key', - valuePlaceholder = 'Value', - isNew, - onChange, - onRemove, - onFocus, - onBlur, -}: KeyValueListInputItemProps): JSX.Element { - const handleKeyChange = useCallback( - ({ value: keyValue }: { value: string }) => { - onChange(index, { key: keyValue, value }); - }, - [index, value, onChange] - ); - - const handleValueChange = useCallback( - ({ value }: { value: string }) => { - onChange(index, { key: keyValue, value }); - }, - [index, keyValue, onChange] - ); - - const handleRemovePress = useCallback(() => { - onRemove(index); - }, [index, onRemove]); - - return ( -
-
- -
- -
- -
- -
- {isNew ? null : ( - - )} -
-
- ); -} - -export default KeyValueListInputItem; diff --git a/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js b/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js index 6e6aad5f9..e7c1535c1 100644 --- a/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js @@ -5,18 +5,19 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { metadataProfileNames } from 'Helpers/Props'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.metadataProfiles', sortByProp('name')), + createSortedSectionSelector('settings.metadataProfiles', sortByName), (state, { includeNoChange }) => includeNoChange, (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, (state, { includeMixed }) => includeMixed, (state, { includeNone }) => includeNone, (metadataProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed, includeNone) => { + const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE); const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE); @@ -38,7 +39,7 @@ function createMapStateToProps() { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } @@ -46,7 +47,7 @@ function createMapStateToProps() { values.unshift({ key: 'mixed', value: '(Mixed)', - isDisabled: true + disabled: true }); } diff --git a/frontend/src/Components/Form/MonitorAlbumsSelectInput.js b/frontend/src/Components/Form/MonitorAlbumsSelectInput.js index d48284c38..a10bbb776 100644 --- a/frontend/src/Components/Form/MonitorAlbumsSelectInput.js +++ b/frontend/src/Components/Form/MonitorAlbumsSelectInput.js @@ -18,15 +18,15 @@ function MonitorAlbumsSelectInput(props) { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } if (includeMixed) { values.unshift({ key: 'mixed', - value: `(${translate('Mixed')})`, - isDisabled: true + value: '(Mixed)', + disabled: true }); } diff --git a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js index 0dccc44a4..f9cc07d7d 100644 --- a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js +++ b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js @@ -18,7 +18,7 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } @@ -26,7 +26,7 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'mixed', value: '(Mixed)', - isDisabled: true + disabled: true }); } diff --git a/frontend/src/Components/Form/PasswordInput.css b/frontend/src/Components/Form/PasswordInput.css new file mode 100644 index 000000000..6cb162784 --- /dev/null +++ b/frontend/src/Components/Form/PasswordInput.css @@ -0,0 +1,5 @@ +.input { + composes: input from '~Components/Form/TextInput.css'; + + font-family: $passwordFamily; +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css.d.ts b/frontend/src/Components/Form/PasswordInput.css.d.ts similarity index 89% rename from frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css.d.ts rename to frontend/src/Components/Form/PasswordInput.css.d.ts index 2c598cbee..774807ef4 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css.d.ts +++ b/frontend/src/Components/Form/PasswordInput.css.d.ts @@ -1,7 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'icon': string; + 'input': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Components/Form/PasswordInput.js b/frontend/src/Components/Form/PasswordInput.js index dbc4cfdb4..fef54fd5a 100644 --- a/frontend/src/Components/Form/PasswordInput.js +++ b/frontend/src/Components/Form/PasswordInput.js @@ -1,5 +1,7 @@ +import PropTypes from 'prop-types'; import React from 'react'; import TextInput from './TextInput'; +import styles from './PasswordInput.css'; // Prevent a user from copying (or cutting) the password from the input function onCopy(e) { @@ -11,14 +13,17 @@ function PasswordInput(props) { return ( ); } PasswordInput.propTypes = { - ...TextInput.props + className: PropTypes.string.isRequired +}; + +PasswordInput.defaultProps = { + className: styles.input }; export default PasswordInput; diff --git a/frontend/src/Components/Form/PlaylistInput.js b/frontend/src/Components/Form/PlaylistInput.js index 0b3966f60..77718f4f1 100644 --- a/frontend/src/Components/Form/PlaylistInput.js +++ b/frontend/src/Components/Form/PlaylistInput.js @@ -9,6 +9,7 @@ import TableBody from 'Components/Table/TableBody'; import TableRow from 'Components/Table/TableRow'; import tagShape from 'Helpers/Props/Shapes/tagShape'; import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; import styles from './PlaylistInput.css'; @@ -45,17 +46,7 @@ class PlaylistInput extends Component { onChange } = this.props; - const oldSelected = _.reduce( - prevState.selectedState, - (result, value, id) => { - if (value) { - result.push(id); - } - - return result; - }, - [] - ).sort(); + const oldSelected = getSelectedIds(prevState.selectedState, { parseIds: false }).sort(); const newSelected = this.getSelectedIds().sort(); if (!_.isEqual(oldSelected, newSelected)) { @@ -70,17 +61,7 @@ class PlaylistInput extends Component { // Control getSelectedIds = () => { - return _.reduce( - this.state.selectedState, - (result, value, id) => { - if (value) { - result.push(id); - } - - return result; - }, - [] - ); + return getSelectedIds(this.state.selectedState, { parseIds: false }); }; // diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index 311f1bbbd..637a80c1c 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -14,8 +14,6 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.CHECK; case 'device': return inputTypes.DEVICE; - case 'keyValueList': - return inputTypes.KEY_VALUE_LIST; case 'playlist': return inputTypes.PLAYLIST; case 'password': @@ -31,8 +29,6 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.DYNAMIC_SELECT; } return inputTypes.SELECT; - case 'artistTag': - return inputTypes.ARTIST_TAG; case 'tag': return inputTypes.TEXT_TAG; case 'tagSelect': @@ -41,12 +37,6 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.TEXT; case 'oAuth': return inputTypes.OAUTH; - case 'rootFolder': - return inputTypes.ROOT_FOLDER_SELECT; - case 'qualityProfile': - return inputTypes.QUALITY_PROFILE_SELECT; - case 'metadataProfile': - return inputTypes.METADATA_PROFILE_SELECT; default: return inputTypes.TEXT; } diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index d7719969a..a898de4a2 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -4,13 +4,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), + createSortedSectionSelector('settings.qualityProfiles', sortByName), (state, { includeNoChange }) => includeNoChange, (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, (state, { includeMixed }) => includeMixed, @@ -26,7 +26,7 @@ function createMapStateToProps() { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } @@ -34,7 +34,7 @@ function createMapStateToProps() { values.unshift({ key: 'mixed', value: '(Mixed)', - isDisabled: true + disabled: true }); } diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js index dcc2b88a6..62077d68a 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js +++ b/frontend/src/Components/Form/RootFolderSelectInputConnector.js @@ -2,19 +2,17 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector'; import RootFolderSelectInput from './RootFolderSelectInput'; const ADD_NEW_KEY = 'addNew'; function createMapStateToProps() { return createSelector( - createRootFoldersSelector(), + (state) => state.settings.rootFolders, (state, { value }) => value, (state, { includeMissingValue }) => includeMissingValue, (state, { includeNoChange }) => includeNoChange, - (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, - (rootFolders, value, includeMissingValue, includeNoChange, includeNoChangeDisabled = true) => { + (rootFolders, value, includeMissingValue, includeNoChange) => { const values = rootFolders.items.map((rootFolder) => { return { key: rootFolder.path, @@ -28,8 +26,9 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - value: 'No Change', - isDisabled: includeNoChangeDisabled, + value: '', + name: 'No Change', + isDisabled: true, isMissing: false }); } @@ -47,6 +46,7 @@ function createMapStateToProps() { values.push({ key: '', value: '', + name: '', isDisabled: true, isHidden: true }); @@ -54,7 +54,8 @@ function createMapStateToProps() { values.push({ key: ADD_NEW_KEY, - value: 'Add a new path' + value: '', + name: 'Add a new path' }); return { diff --git a/frontend/src/Components/Form/SelectInput.js b/frontend/src/Components/Form/SelectInput.js index d43560134..553501afc 100644 --- a/frontend/src/Components/Form/SelectInput.js +++ b/frontend/src/Components/Form/SelectInput.js @@ -52,7 +52,6 @@ class SelectInput extends Component { const { key, value: optionValue, - isDisabled: optionIsDisabled = false, ...otherOptionProps } = option; @@ -60,7 +59,6 @@ class SelectInput extends Component {
diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js new file mode 100644 index 000000000..5e5dda730 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; +import { align, icons, kinds } from 'Helpers/Props'; +import styles from './PageHeaderActionsMenu.css'; + +function PageHeaderActionsMenu(props) { + const { + formsAuth, + onKeyboardShortcutsPress, + onRestartPress, + onShutdownPress + } = props; + + return ( +
+ + + + + + + + + Keyboard Shortcuts + + + + + + + Restart + + + + + Shutdown + + + { + formsAuth && +
+ } + + { + formsAuth && + + + Logout + + } + +
+
+ ); +} + +PageHeaderActionsMenu.propTypes = { + formsAuth: PropTypes.bool.isRequired, + onKeyboardShortcutsPress: PropTypes.func.isRequired, + onRestartPress: PropTypes.func.isRequired, + onShutdownPress: PropTypes.func.isRequired +}; + +export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx deleted file mode 100644 index 7a0c35c1c..000000000 --- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import Icon from 'Components/Icon'; -import Menu from 'Components/Menu/Menu'; -import MenuButton from 'Components/Menu/MenuButton'; -import MenuContent from 'Components/Menu/MenuContent'; -import MenuItem from 'Components/Menu/MenuItem'; -import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; -import { align, icons, kinds } from 'Helpers/Props'; -import { restart, shutdown } from 'Store/Actions/systemActions'; -import translate from 'Utilities/String/translate'; -import styles from './PageHeaderActionsMenu.css'; - -interface PageHeaderActionsMenuProps { - onKeyboardShortcutsPress(): void; -} - -function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) { - const { onKeyboardShortcutsPress } = props; - - const dispatch = useDispatch(); - - const { authentication, isDocker } = useSelector( - (state: AppState) => state.system.status.item - ); - - const formsAuth = authentication === 'forms'; - - const handleRestartPress = useCallback(() => { - dispatch(restart()); - }, [dispatch]); - - const handleShutdownPress = useCallback(() => { - dispatch(shutdown()); - }, [dispatch]); - - return ( -
- - - - - - - - - {translate('KeyboardShortcuts')} - - - {isDocker ? null : ( - <> - - - - - {translate('Restart')} - - - - - {translate('Shutdown')} - - - )} - - {formsAuth ? ( - <> - - - - - {translate('Logout')} - - - ) : null} - - -
- ); -} - -export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js new file mode 100644 index 000000000..3aba95065 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { restart, shutdown } from 'Store/Actions/systemActions'; +import PageHeaderActionsMenu from './PageHeaderActionsMenu'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.status, + (status) => { + return { + formsAuth: status.item.authentication === 'forms' + }; + } + ); +} + +const mapDispatchToProps = { + restart, + shutdown +}; + +class PageHeaderActionsMenuConnector extends Component { + + // + // Listeners + + onRestartPress = () => { + this.props.restart(); + }; + + onShutdownPress = () => { + this.props.shutdown(); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +PageHeaderActionsMenuConnector.propTypes = { + restart: PropTypes.func.isRequired, + shutdown: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector); diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js index 4b24a8231..eac6d709f 100644 --- a/frontend/src/Components/Page/Page.js +++ b/frontend/src/Components/Page/Page.js @@ -4,7 +4,6 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; import ColorImpairedContext from 'App/ColorImpairedContext'; import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; import SignalRConnector from 'Components/SignalRConnector'; -import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import PageHeader from './Header/PageHeader'; import PageSidebar from './Sidebar/PageSidebar'; @@ -76,7 +75,6 @@ class Page extends Component { isSmallScreen, isSidebarVisible, enableColorImpairedMode, - authenticationEnabled, onSidebarToggle, onSidebarVisibleChange } = this.props; @@ -110,10 +108,6 @@ class Page extends Component { isOpen={this.state.isConnectionLostModalOpen} onModalClose={this.onConnectionLostModalClose} /> - -
); @@ -129,7 +123,6 @@ Page.propTypes = { isUpdated: PropTypes.bool.isRequired, isDisconnected: PropTypes.bool.isRequired, enableColorImpairedMode: PropTypes.bool.isRequired, - authenticationEnabled: PropTypes.bool.isRequired, onResize: PropTypes.func.isRequired, onSidebarToggle: PropTypes.func.isRequired, onSidebarVisibleChange: PropTypes.func.isRequired diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index c84099a5e..b13695f17 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -6,18 +6,10 @@ import { createSelector } from 'reselect'; import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchArtist } from 'Store/Actions/artistActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; -import { - fetchImportLists, - fetchIndexerFlags, - fetchLanguages, - fetchMetadataProfiles, - fetchQualityProfiles, - fetchUISettings -} from 'Store/Actions/settingsActions'; +import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchTags } from 'Store/Actions/tagActions'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import ErrorPage from './ErrorPage'; import LoadingPage from './LoadingPage'; import Page from './Page'; @@ -51,7 +43,6 @@ const selectAppProps = createSelector( ); const selectIsPopulated = createSelector( - (state) => state.artist.isPopulated, (state) => state.customFilters.isPopulated, (state) => state.tags.isPopulated, (state) => state.settings.ui.isPopulated, @@ -59,11 +50,9 @@ const selectIsPopulated = createSelector( (state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.importLists.isPopulated, - (state) => state.settings.indexerFlags.isPopulated, (state) => state.system.status.isPopulated, (state) => state.app.translations.isPopulated, ( - artistsIsPopulated, customFiltersIsPopulated, tagsIsPopulated, uiSettingsIsPopulated, @@ -71,12 +60,10 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated, metadataProfilesIsPopulated, importListsIsPopulated, - indexerFlagsIsPopulated, systemStatusIsPopulated, translationsIsPopulated ) => { return ( - artistsIsPopulated && customFiltersIsPopulated && tagsIsPopulated && uiSettingsIsPopulated && @@ -84,7 +71,6 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated && metadataProfilesIsPopulated && importListsIsPopulated && - indexerFlagsIsPopulated && systemStatusIsPopulated && translationsIsPopulated ); @@ -92,7 +78,6 @@ const selectIsPopulated = createSelector( ); const selectErrors = createSelector( - (state) => state.artist.error, (state) => state.customFilters.error, (state) => state.tags.error, (state) => state.settings.ui.error, @@ -100,11 +85,9 @@ const selectErrors = createSelector( (state) => state.settings.qualityProfiles.error, (state) => state.settings.metadataProfiles.error, (state) => state.settings.importLists.error, - (state) => state.settings.indexerFlags.error, (state) => state.system.status.error, (state) => state.app.translations.error, ( - artistsError, customFiltersError, tagsError, uiSettingsError, @@ -112,12 +95,10 @@ const selectErrors = createSelector( qualityProfilesError, metadataProfilesError, importListsError, - indexerFlagsError, systemStatusError, translationsError ) => { const hasError = !!( - artistsError || customFiltersError || tagsError || uiSettingsError || @@ -125,7 +106,6 @@ const selectErrors = createSelector( qualityProfilesError || metadataProfilesError || importListsError || - indexerFlagsError || systemStatusError || translationsError ); @@ -139,7 +119,6 @@ const selectErrors = createSelector( qualityProfilesError, metadataProfilesError, importListsError, - indexerFlagsError, systemStatusError, translationsError }; @@ -153,21 +132,18 @@ function createMapStateToProps() { selectErrors, selectAppProps, createDimensionsSelector(), - createSystemStatusSelector(), ( enableColorImpairedMode, isPopulated, errors, app, - dimensions, - systemStatus + dimensions ) => { return { ...app, ...errors, isPopulated, isSmallScreen: dimensions.isSmallScreen, - authenticationEnabled: systemStatus.authentication !== 'none', enableColorImpairedMode }; } @@ -197,9 +173,6 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchImportLists() { dispatch(fetchImportLists()); }, - dispatchFetchIndexerFlags() { - dispatch(fetchIndexerFlags()); - }, dispatchFetchUISettings() { dispatch(fetchUISettings()); }, @@ -240,7 +213,6 @@ class PageConnector extends Component { this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchMetadataProfiles(); this.props.dispatchFetchImportLists(); - this.props.dispatchFetchIndexerFlags(); this.props.dispatchFetchUISettings(); this.props.dispatchFetchStatus(); this.props.dispatchFetchTranslations(); @@ -267,7 +239,6 @@ class PageConnector extends Component { dispatchFetchQualityProfiles, dispatchFetchMetadataProfiles, dispatchFetchImportLists, - dispatchFetchIndexerFlags, dispatchFetchUISettings, dispatchFetchStatus, dispatchFetchTranslations, @@ -309,7 +280,6 @@ PageConnector.propTypes = { dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchMetadataProfiles: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, - dispatchFetchIndexerFlags: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchTranslations: PropTypes.func.isRequired, diff --git a/frontend/src/Components/Page/PageContentBody.js b/frontend/src/Components/Page/PageContentBody.js new file mode 100644 index 000000000..1c93e575b --- /dev/null +++ b/frontend/src/Components/Page/PageContentBody.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Scroller from 'Components/Scroller/Scroller'; +import { scrollDirections } from 'Helpers/Props'; +import { isLocked } from 'Utilities/scrollLock'; +import styles from './PageContentBody.css'; + +class PageContentBody extends Component { + + // + // Listeners + + onScroll = (props) => { + const { onScroll } = this.props; + + if (this.props.onScroll && !isLocked()) { + onScroll(props); + } + }; + + // + // Render + + render() { + const { + className, + innerClassName, + children, + dispatch, + ...otherProps + } = this.props; + + return ( + +
+ {children} +
+
+ ); + } +} + +PageContentBody.propTypes = { + className: PropTypes.string, + innerClassName: PropTypes.string, + children: PropTypes.node.isRequired, + onScroll: PropTypes.func, + dispatch: PropTypes.func +}; + +PageContentBody.defaultProps = { + className: styles.contentBody, + innerClassName: styles.innerContentBody +}; + +export default PageContentBody; diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx deleted file mode 100644 index ce9b0e7e4..000000000 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react'; -import Scroller, { OnScroll } from 'Components/Scroller/Scroller'; -import ScrollDirection from 'Helpers/Props/ScrollDirection'; -import { isLocked } from 'Utilities/scrollLock'; -import styles from './PageContentBody.css'; - -interface PageContentBodyProps { - className?: string; - innerClassName?: string; - children: ReactNode; - initialScrollTop?: number; - onScroll?: (payload: OnScroll) => void; -} - -const PageContentBody = forwardRef( - (props: PageContentBodyProps, ref: ForwardedRef) => { - const { - className = styles.contentBody, - innerClassName = styles.innerContentBody, - children, - onScroll, - ...otherProps - } = props; - - const onScrollWrapper = useCallback( - (payload: OnScroll) => { - if (onScroll && !isLocked()) { - onScroll(payload); - } - }, - [onScroll] - ); - - return ( - -
{children}
-
- ); - } -); - -export default PageContentBody; diff --git a/frontend/src/Components/Page/PageContentFooter.css b/frontend/src/Components/Page/PageContentFooter.css index 61c63064a..4709af871 100644 --- a/frontend/src/Components/Page/PageContentFooter.css +++ b/frontend/src/Components/Page/PageContentFooter.css @@ -8,6 +8,14 @@ @media only screen and (max-width: $breakpointSmall) { .contentFooter { display: block; + + div { + margin-top: 10px; + + &:first-child { + margin-top: 0; + } + } } } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index d6db8d612..583a604bd 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -29,6 +29,14 @@ const links = [ title: () => translate('AddNew'), to: '/add/search' }, + { + title: () => translate('MassEditor'), + to: '/artisteditor' + }, + { + title: () => translate('AlbumStudio'), + to: '/albumstudio' + }, { title: () => translate('UnmappedFiles'), to: '/unmapped' @@ -129,7 +137,7 @@ const links = [ to: '/settings/general' }, { - title: () => translate('Ui'), + title: () => translate('UI'), to: '/settings/ui' } ] diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js index c93603aa9..2d179396a 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js @@ -45,8 +45,7 @@ PageToolbarButton.propTypes = { iconName: PropTypes.object.isRequired, spinningName: PropTypes.object, isSpinning: PropTypes.bool, - isDisabled: PropTypes.bool, - onPress: PropTypes.func + isDisabled: PropTypes.bool }; PageToolbarButton.defaultProps = { diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css b/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css deleted file mode 100644 index b3cae8163..000000000 --- a/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.css +++ /dev/null @@ -1,3 +0,0 @@ -.icon { - margin-right: 8px; -} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.tsx b/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.tsx deleted file mode 100644 index c97eb2a91..000000000 --- a/frontend/src/Components/Page/Toolbar/PageToolbarOverflowMenuItem.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { IconDefinition } from '@fortawesome/fontawesome-common-types'; -import React from 'react'; -import MenuItem from 'Components/Menu/MenuItem'; -import SpinnerIcon from 'Components/SpinnerIcon'; -import styles from './PageToolbarOverflowMenuItem.css'; - -interface PageToolbarOverflowMenuItemProps { - iconName: IconDefinition; - spinningName?: IconDefinition; - isDisabled?: boolean; - isSpinning?: boolean; - showIndicator?: boolean; - label: string; - text?: string; - onPress: () => void; -} - -function PageToolbarOverflowMenuItem(props: PageToolbarOverflowMenuItemProps) { - const { - iconName, - spinningName, - label, - isDisabled, - isSpinning = false, - ...otherProps - } = props; - - return ( - - - {label} - - ); -} - -export default PageToolbarOverflowMenuItem; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js index 2d4aca718..d64d11435 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js @@ -4,11 +4,12 @@ import React, { Component } from 'react'; import Measure from 'Components/Measure'; import Menu from 'Components/Menu/Menu'; import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; +import SpinnerIcon from 'Components/SpinnerIcon'; import { forEach } from 'Helpers/elementChildren'; import { align, icons } from 'Helpers/Props'; import dimensions from 'Styles/Variables/dimensions'; -import PageToolbarOverflowMenuItem from './PageToolbarOverflowMenuItem'; import styles from './PageToolbarSection.css'; const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth); @@ -167,15 +168,28 @@ class PageToolbarSection extends Component { { overflowItems.map((item) => { const { + iconName, + spinningName, label, - overflowComponent: OverflowComponent = PageToolbarOverflowMenuItem + isDisabled, + isSpinning, + ...otherProps } = item; return ( - + isDisabled={isDisabled || isSpinning} + {...otherProps} + > + + {label} + ); }) } diff --git a/frontend/src/Components/Scroller/Scroller.js b/frontend/src/Components/Scroller/Scroller.js new file mode 100644 index 000000000..205f1aadd --- /dev/null +++ b/frontend/src/Components/Scroller/Scroller.js @@ -0,0 +1,95 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import styles from './Scroller.css'; + +class Scroller extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scroller = null; + } + + componentDidMount() { + const { + scrollDirection, + autoFocus, + scrollTop + } = this.props; + + if (this.props.scrollTop != null) { + this._scroller.scrollTop = scrollTop; + } + + if (autoFocus && scrollDirection !== scrollDirections.NONE) { + this._scroller.focus({ preventScroll: true }); + } + } + + // + // Control + + _setScrollerRef = (ref) => { + this._scroller = ref; + + this.props.registerScroller(ref); + }; + + // + // Render + + render() { + const { + className, + scrollDirection, + autoScroll, + children, + scrollTop, + onScroll, + registerScroller, + ...otherProps + } = this.props; + + return ( +
+ {children} +
+ ); + } + +} + +Scroller.propTypes = { + className: PropTypes.string, + scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired, + autoFocus: PropTypes.bool.isRequired, + autoScroll: PropTypes.bool.isRequired, + scrollTop: PropTypes.number, + children: PropTypes.node, + onScroll: PropTypes.func, + registerScroller: PropTypes.func +}; + +Scroller.defaultProps = { + scrollDirection: scrollDirections.VERTICAL, + autoFocus: true, + autoScroll: true, + registerScroller: () => { /* no-op */ } +}; + +export default Scroller; diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx deleted file mode 100644 index 37b16eebd..000000000 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import classNames from 'classnames'; -import { throttle } from 'lodash'; -import React, { - ForwardedRef, - forwardRef, - MutableRefObject, - ReactNode, - useEffect, - useRef, -} from 'react'; -import ScrollDirection from 'Helpers/Props/ScrollDirection'; -import styles from './Scroller.css'; - -export interface OnScroll { - scrollLeft: number; - scrollTop: number; -} - -interface ScrollerProps { - className?: string; - scrollDirection?: ScrollDirection; - autoFocus?: boolean; - autoScroll?: boolean; - scrollTop?: number; - initialScrollTop?: number; - children?: ReactNode; - onScroll?: (payload: OnScroll) => void; -} - -const Scroller = forwardRef( - (props: ScrollerProps, ref: ForwardedRef) => { - const { - className, - autoFocus = false, - autoScroll = true, - scrollDirection = ScrollDirection.Vertical, - children, - scrollTop, - initialScrollTop, - onScroll, - ...otherProps - } = props; - - const internalRef = useRef(); - const currentRef = (ref as MutableRefObject) ?? internalRef; - - useEffect( - () => { - if (initialScrollTop != null) { - currentRef.current.scrollTop = initialScrollTop; - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - useEffect(() => { - if (scrollTop != null) { - currentRef.current.scrollTop = scrollTop; - } - - if (autoFocus && scrollDirection !== ScrollDirection.None) { - currentRef.current.focus({ preventScroll: true }); - } - }, [autoFocus, currentRef, scrollDirection, scrollTop]); - - useEffect(() => { - const div = currentRef.current; - - const handleScroll = throttle(() => { - const scrollLeft = div.scrollLeft; - const scrollTop = div.scrollTop; - - onScroll?.({ scrollLeft, scrollTop }); - }, 10); - - div.addEventListener('scroll', handleScroll); - - return () => { - div.removeEventListener('scroll', handleScroll); - }; - }, [currentRef, onScroll]); - - return ( -
- {children} -
- ); - } -); - -export default Scroller; diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 365827a2b..966097730 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -172,7 +172,7 @@ class SignalRConnector extends Component { const status = resource.status; // Both successful and failed commands need to be - // completed, otherwise they spin until they time out. + // completed, otherwise they spin until they timeout. if (status === 'completed' || status === 'failed') { this.props.dispatchFinishCommand(resource); @@ -216,66 +216,16 @@ class SignalRConnector extends Component { this.props.dispatchUpdateItem({ section, ...body.resource }); } else if (body.action === 'deleted') { this.props.dispatchRemoveItem({ section, id: body.resource.id }); - - repopulatePage('trackFileDeleted'); } // Repopulate the page to handle recently imported file repopulatePage('trackFileUpdated'); }; - handleDownloadclient = ({ action, resource }) => { - const section = 'settings.downloadClients'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - handleHealth = () => { this.props.dispatchFetchHealth(); }; - handleImportlist = ({ action, resource }) => { - const section = 'settings.importLists'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleIndexer = ({ action, resource }) => { - const section = 'settings.indexers'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleMetadata = ({ action, resource }) => { - const section = 'settings.metadata'; - - if (action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } - }; - - handleNotification = ({ action, resource }) => { - const section = 'settings.notifications'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - handleArtist = (body) => { const action = body.action; const section = 'artist'; @@ -314,7 +264,7 @@ class SignalRConnector extends Component { handleWantedCutoff = (body) => { if (body.action === 'updated') { this.props.dispatchUpdateItem({ - section: 'wanted.cutoffUnmet', + section: 'cutoffUnmet', updateOnly: true, ...body.resource }); @@ -324,7 +274,7 @@ class SignalRConnector extends Component { handleWantedMissing = (body) => { if (body.action === 'updated') { this.props.dispatchUpdateItem({ - section: 'wanted.missing', + section: 'missing', updateOnly: true, ...body.resource }); diff --git a/frontend/src/Components/SpinnerIcon.js b/frontend/src/Components/SpinnerIcon.js index 5ae03ee66..d21674d9e 100644 --- a/frontend/src/Components/SpinnerIcon.js +++ b/frontend/src/Components/SpinnerIcon.js @@ -21,7 +21,6 @@ function SpinnerIcon(props) { } SpinnerIcon.propTypes = { - className: PropTypes.string, name: PropTypes.object.isRequired, spinningName: PropTypes.object.isRequired, isSpinning: PropTypes.bool.isRequired diff --git a/frontend/src/Components/Table/Cells/TableRowCell.css b/frontend/src/Components/Table/Cells/TableRowCell.css index 7e3353c25..47ce0d22e 100644 --- a/frontend/src/Components/Table/Cells/TableRowCell.css +++ b/frontend/src/Components/Table/Cells/TableRowCell.css @@ -4,7 +4,7 @@ line-height: 1.52857143; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .cell { white-space: nowrap; } diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.css b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css index f7f3b9306..2501b7c84 100644 --- a/frontend/src/Components/Table/Cells/VirtualTableRowCell.css +++ b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css @@ -7,7 +7,7 @@ white-space: nowrap; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .cell { white-space: nowrap; } diff --git a/frontend/src/Components/Table/Table.css b/frontend/src/Components/Table/Table.css index d0507be6b..bdfdec641 100644 --- a/frontend/src/Components/Table/Table.css +++ b/frontend/src/Components/Table/Table.css @@ -10,7 +10,7 @@ border-collapse: collapse; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .tableContainer { min-width: 100%; width: fit-content; diff --git a/frontend/src/Components/Table/TableHeaderCell.css b/frontend/src/Components/Table/TableHeaderCell.css index eded9c95b..c2c4f58c8 100644 --- a/frontend/src/Components/Table/TableHeaderCell.css +++ b/frontend/src/Components/Table/TableHeaderCell.css @@ -9,7 +9,7 @@ margin-left: 10px; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .headerCell { white-space: nowrap; } diff --git a/frontend/src/Components/Table/TablePager.css b/frontend/src/Components/Table/TablePager.css index 6d184196e..d73a0d0c0 100644 --- a/frontend/src/Components/Table/TablePager.css +++ b/frontend/src/Components/Table/TablePager.css @@ -60,7 +60,7 @@ height: 25px; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .pager { flex-wrap: wrap; } diff --git a/frontend/src/Components/Table/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js index 5473413cb..4a597e795 100644 --- a/frontend/src/Components/Table/VirtualTable.js +++ b/frontend/src/Components/Table/VirtualTable.js @@ -7,8 +7,6 @@ import { scrollDirections } from 'Helpers/Props'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import styles from './VirtualTable.css'; -const ROW_HEIGHT = 38; - function overscanIndicesGetter(options) { const { cellCount, @@ -50,7 +48,8 @@ class VirtualTable extends Component { const { items, scrollIndex, - scrollTop + scrollTop, + onRecompute } = this.props; const { @@ -58,7 +57,10 @@ class VirtualTable extends Component { scrollRestored } = this.state; - if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) { + if (this._grid && + (prevState.width !== width || + hasDifferentItemsOrOrder(prevProps.items, items))) { + onRecompute(width); // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } @@ -101,6 +103,7 @@ class VirtualTable extends Component { className, items, scroller, + scrollTop: ignored, header, headerHeight, rowHeight, @@ -146,7 +149,6 @@ class VirtualTable extends Component { {header}
@@ -189,14 +192,16 @@ VirtualTable.propTypes = { scroller: PropTypes.instanceOf(Element).isRequired, header: PropTypes.node.isRequired, headerHeight: PropTypes.number.isRequired, + rowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]).isRequired, rowRenderer: PropTypes.func.isRequired, - rowHeight: PropTypes.number.isRequired + onRecompute: PropTypes.func.isRequired }; VirtualTable.defaultProps = { className: styles.tableContainer, headerHeight: 38, - rowHeight: ROW_HEIGHT + rowHeight: 38, + onRecompute: () => {} }; export default VirtualTable; diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.css b/frontend/src/Components/Table/VirtualTableHeaderCell.css index eded9c95b..c2c4f58c8 100644 --- a/frontend/src/Components/Table/VirtualTableHeaderCell.css +++ b/frontend/src/Components/Table/VirtualTableHeaderCell.css @@ -9,7 +9,7 @@ margin-left: 10px; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .headerCell { white-space: nowrap; } diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js index fe700b8fe..6da96849c 100644 --- a/frontend/src/Components/TagList.js +++ b/frontend/src/Components/TagList.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { kinds } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; import Label from './Label'; import styles from './TagList.css'; @@ -9,7 +8,7 @@ function TagList({ tags, tagList }) { const sortedTags = tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) .filter((tag) => !!tag) - .sort(sortByProp('label')); + .sort((a, b) => a.label.localeCompare(b.label)); return (
diff --git a/frontend/src/Components/withScrollPosition.js b/frontend/src/Components/withScrollPosition.js new file mode 100644 index 000000000..bb089b8b0 --- /dev/null +++ b/frontend/src/Components/withScrollPosition.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import scrollPositions from 'Store/scrollPositions'; + +function withScrollPosition(WrappedComponent, scrollPositionKey) { + function ScrollPosition(props) { + const { + history + } = props; + + const scrollTop = history.action === 'POP' || (history.location.state && history.location.state.restoreScrollPosition) ? + scrollPositions[scrollPositionKey] : + 0; + + return ( + + ); + } + + ScrollPosition.propTypes = { + history: PropTypes.object.isRequired + }; + + return ScrollPosition; +} + +export default withScrollPosition; diff --git a/frontend/src/Components/withScrollPosition.tsx b/frontend/src/Components/withScrollPosition.tsx deleted file mode 100644 index f688a6253..000000000 --- a/frontend/src/Components/withScrollPosition.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; -import scrollPositions from 'Store/scrollPositions'; - -interface WrappedComponentProps { - initialScrollTop: number; -} - -interface ScrollPositionProps { - history: RouteComponentProps['history']; - location: RouteComponentProps['location']; - match: RouteComponentProps['match']; -} - -function withScrollPosition( - WrappedComponent: React.FC, - scrollPositionKey: string -) { - function ScrollPosition(props: ScrollPositionProps) { - const { history } = props; - - const initialScrollTop = - history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0; - - return ; - } - - return ScrollPosition; -} - -export default withScrollPosition; diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css index e0f1bf5dc..bf31501dd 100644 --- a/frontend/src/Content/Fonts/fonts.css +++ b/frontend/src/Content/Fonts/fonts.css @@ -25,3 +25,14 @@ font-family: 'Ubuntu Mono'; src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype'); } + +/* + * text-security-disc + */ + +@font-face { + font-weight: normal; + font-style: normal; + font-family: 'text-security-disc'; + src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype'); +} diff --git a/frontend/src/Content/Fonts/text-security-disc.ttf b/frontend/src/Content/Fonts/text-security-disc.ttf new file mode 100644 index 000000000..86038dba8 Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.ttf differ diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff new file mode 100644 index 000000000..bc4cc324b Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.woff differ diff --git a/frontend/src/Content/Images/Icons/browserconfig.xml b/frontend/src/Content/Images/Icons/browserconfig.xml new file mode 100644 index 000000000..993924968 --- /dev/null +++ b/frontend/src/Content/Images/Icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #00ccff + + + diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json new file mode 100644 index 000000000..cff971235 --- /dev/null +++ b/frontend/src/Content/Images/Icons/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Lidarr", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "../../../../", + "theme_color": "#3a3f51", + "background_color": "#3a3f51", + "display": "standalone" +} diff --git a/frontend/src/Content/browserconfig.xml b/frontend/src/Content/browserconfig.xml deleted file mode 100644 index 646112d06..000000000 --- a/frontend/src/Content/browserconfig.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - #00ccff - - - - diff --git a/frontend/src/Content/manifest.json b/frontend/src/Content/manifest.json deleted file mode 100644 index 5c2b3d59d..000000000 --- a/frontend/src/Content/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "__INSTANCE_NAME__", - "icons": [ - { - "src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "__URL_BASE__/Content/Images/Icons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "start_url": "__URL_BASE__/", - "theme_color": "#3a3f51", - "background_color": "#3a3f51", - "display": "standalone" -} diff --git a/frontend/src/Diag/ConsoleApi.js b/frontend/src/Diag/ConsoleApi.js index 502c243cc..117b54104 100644 --- a/frontend/src/Diag/ConsoleApi.js +++ b/frontend/src/Diag/ConsoleApi.js @@ -81,8 +81,7 @@ class ResourceApi { class ConsoleApi { constructor() { - this.artist = new ResourceApi(this, '/artist'); - this.album = new ResourceApi(this, '/album'); + this.series = new ResourceApi(this, '/artist'); } resource(url) { diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.js b/frontend/src/FirstRun/AuthenticationRequiredModal.js deleted file mode 100644 index caa855cb7..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModal.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModal(props) { - const { - isOpen - } = props; - - return ( - - - - ); -} - -AuthenticationRequiredModal.propTypes = { - isOpen: PropTypes.bool.isRequired -}; - -export default AuthenticationRequiredModal; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.css b/frontend/src/FirstRun/AuthenticationRequiredModalContent.css deleted file mode 100644 index bbc6704e6..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.css +++ /dev/null @@ -1,5 +0,0 @@ -.authRequiredAlert { - composes: alert from '~Components/Alert.css'; - - margin-bottom: 20px; -} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.css.d.ts b/frontend/src/FirstRun/AuthenticationRequiredModalContent.css.d.ts deleted file mode 100644 index 9454d5428..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'authRequiredAlert': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js deleted file mode 100644 index 568cc66af..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js +++ /dev/null @@ -1,170 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useEffect, useRef } from 'react'; -import Alert from 'Components/Alert'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds } from 'Helpers/Props'; -import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings'; -import translate from 'Utilities/String/translate'; -import styles from './AuthenticationRequiredModalContent.css'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModalContent(props) { - const { - isPopulated, - error, - isSaving, - settings, - onInputChange, - onSavePress, - dispatchFetchStatus - } = props; - - const { - authenticationMethod, - authenticationRequired, - username, - password, - passwordConfirmation - } = settings; - - const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; - - const didMount = useRef(false); - - useEffect(() => { - if (!isSaving && didMount.current) { - dispatchFetchStatus(); - } - - didMount.current = true; - }, [isSaving, dispatchFetchStatus]); - - return ( - - - {translate('AuthenticationRequired')} - - - - - {translate('AuthenticationRequiredWarning')} - - - { - isPopulated && !error ? -
- - {translate('AuthenticationMethod')} - - - - - - {translate('AuthenticationRequired')} - - - - - - {translate('Username')} - - - - - - {translate('Password')} - - - - - - {translate('PasswordConfirmation')} - - - -
: - null - } - - { - !isPopulated && !error ? : null - } -
- - - - {translate('Save')} - - -
- ); -} - -AuthenticationRequiredModalContent.propTypes = { - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - settings: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default AuthenticationRequiredModalContent; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js b/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js deleted file mode 100644 index 6653a9d34..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js +++ /dev/null @@ -1,86 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions'; -import { fetchStatus } from 'Store/Actions/systemActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; - -const SECTION = 'general'; - -function createMapStateToProps() { - return createSelector( - createSettingsSectionSelector(SECTION), - (sectionSettings) => { - return { - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchClearPendingChanges: clearPendingChanges, - dispatchSetGeneralSettingsValue: setGeneralSettingsValue, - dispatchSaveGeneralSettings: saveGeneralSettings, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchFetchStatus: fetchStatus -}; - -class AuthenticationRequiredModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchGeneralSettings(); - } - - componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetGeneralSettingsValue({ name, value }); - }; - - onSavePress = () => { - this.props.dispatchSaveGeneralSettings(); - }; - - // - // Render - - render() { - const { - dispatchClearPendingChanges, - dispatchFetchGeneralSettings, - dispatchSetGeneralSettingsValue, - dispatchSaveGeneralSettings, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -AuthenticationRequiredModalContentConnector.propTypes = { - dispatchClearPendingChanges: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchSetGeneralSettingsValue: PropTypes.func.isRequired, - dispatchSaveGeneralSettings: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector); diff --git a/frontend/src/Helpers/Hooks/useMeasure.ts b/frontend/src/Helpers/Hooks/useMeasure.ts deleted file mode 100644 index 7b36b2844..000000000 --- a/frontend/src/Helpers/Hooks/useMeasure.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer'; -import { - default as useMeasureHook, - Options, - RectReadOnly, -} from 'react-use-measure'; - -const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill; - -export type Measurements = RectReadOnly; - -function useMeasure( - options?: Omit -): ReturnType { - return useMeasureHook({ - polyfill: ResizeObserver, - ...options, - }); -} - -export default useMeasure; diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts deleted file mode 100644 index f5b5a96f0..000000000 --- a/frontend/src/Helpers/Hooks/useModalOpenState.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback, useState } from 'react'; - -export default function useModalOpenState( - initialState: boolean -): [boolean, () => void, () => void] { - const [isOpen, setOpen] = useState(initialState); - - const setModalOpen = useCallback(() => { - setOpen(true); - }, [setOpen]); - - const setModalClosed = useCallback(() => { - setOpen(false); - }, [setOpen]); - - return [isOpen, setModalOpen, setModalClosed]; -} diff --git a/frontend/src/Helpers/Props/ScrollDirection.ts b/frontend/src/Helpers/Props/ScrollDirection.ts deleted file mode 100644 index 0da932d22..000000000 --- a/frontend/src/Helpers/Props/ScrollDirection.ts +++ /dev/null @@ -1,8 +0,0 @@ -enum ScrollDirection { - Horizontal = 'horizontal', - Vertical = 'vertical', - None = 'none', - Both = 'both', -} - -export default ScrollDirection; diff --git a/frontend/src/Helpers/Props/filterBuilderTypes.js b/frontend/src/Helpers/Props/filterBuilderTypes.js index c0806fabc..f2ef6f2f8 100644 --- a/frontend/src/Helpers/Props/filterBuilderTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderTypes.js @@ -1,18 +1,14 @@ import * as filterTypes from './filterTypes'; export const ARRAY = 'array'; -export const CONTAINS = 'contains'; export const DATE = 'date'; -export const EQUAL = 'equal'; export const EXACT = 'exact'; export const NUMBER = 'number'; export const STRING = 'string'; export const all = [ ARRAY, - CONTAINS, DATE, - EQUAL, EXACT, NUMBER, STRING @@ -24,10 +20,6 @@ export const possibleFilterTypes = { { key: filterTypes.NOT_CONTAINS, value: 'does not contain' } ], - [CONTAINS]: [ - { key: filterTypes.CONTAINS, value: 'contains' } - ], - [DATE]: [ { key: filterTypes.LESS_THAN, value: 'is before' }, { key: filterTypes.GREATER_THAN, value: 'is after' }, @@ -37,10 +29,6 @@ export const possibleFilterTypes = { { key: filterTypes.NOT_IN_NEXT, value: 'not in the next' } ], - [EQUAL]: [ - { key: filterTypes.EQUAL, value: 'is' } - ], - [EXACT]: [ { key: filterTypes.EQUAL, value: 'is' }, { key: filterTypes.NOT_EQUAL, value: 'is not' } diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index 005ea0b7a..42df49eda 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -2,13 +2,10 @@ export const BOOL = 'bool'; export const BYTES = 'bytes'; export const DATE = 'date'; export const DEFAULT = 'default'; -export const HISTORY_EVENT_TYPE = 'historyEventType'; export const INDEXER = 'indexer'; export const METADATA_PROFILE = 'metadataProfile'; -export const MONITOR_NEW_ITEMS = 'monitorNewItems'; export const PROTOCOL = 'protocol'; export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; -export const ARTIST = 'artist'; export const ARTIST_STATUS = 'artistStatus'; export const TAG = 'tag'; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index aa9c23145..29fa87780 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -15,8 +15,7 @@ import { faHdd as farHdd, faKeyboard as farKeyboard, faObjectGroup as farObjectGroup, - faObjectUngroup as farObjectUngroup, - faSquare as farSquare + faObjectUngroup as farObjectUngroup } from '@fortawesome/free-regular-svg-icons'; // // Solid @@ -32,7 +31,6 @@ import { faBookReader as fasBookReader, faBroadcastTower as fasBroadcastTower, faBug as fasBug, - faCalculator as fasCalculator, faCalendarAlt as fasCalendarAlt, faCaretDown as fasCaretDown, faCheck as fasCheck, @@ -56,12 +54,10 @@ import { faEye as fasEye, faFastBackward as fasFastBackward, faFastForward as fasFastForward, - faFileCircleQuestion as fasFileCircleQuestion, faFileExport as fasFileExport, faFileImport as fasFileImport, faFileInvoice as farFileInvoice, faFilter as fasFilter, - faFlag as fasFlag, faFolderOpen as fasFolderOpen, faForward as fasForward, faHeart as fasHeart, @@ -94,8 +90,6 @@ import { faSortDown as fasSortDown, faSortUp as fasSortUp, faSpinner as fasSpinner, - faSquareCheck as fasSquareCheck, - faSquareMinus as fasSquareMinus, faStar as fasStar, faStop as fasStop, faSync as fasSync, @@ -134,7 +128,6 @@ export const CARET_DOWN = fasCaretDown; export const CHECK = fasCheck; export const CHECK_INDETERMINATE = fasMinus; export const CHECK_CIRCLE = fasCheckCircle; -export const CHECK_SQUARE = fasSquareCheck; export const CIRCLE = fasCircle; export const CIRCLE_OUTLINE = farCircle; export const CLEAR = fasTrashAlt; @@ -157,10 +150,8 @@ export const EXPORT = fasFileExport; export const EXTERNAL_LINK = fasExternalLinkAlt; export const FATAL = fasTimesCircle; export const FILE = farFile; -export const FILE_IMPORT = fasFileImport; -export const FILE_MISSING = fasFileCircleQuestion; +export const FILEIMPORT = fasFileImport; export const FILTER = fasFilter; -export const FLAG = fasFlag; export const FOLDER = farFolder; export const FOLDER_OPEN = fasFolderOpen; export const GROUP = farObjectGroup; @@ -188,7 +179,6 @@ export const PAGE_PREVIOUS = fasBackward; export const PAGE_NEXT = fasForward; export const PAGE_LAST = fasFastForward; export const PARENT = fasLevelUpAlt; -export const PARSE = fasCalculator; export const PAUSED = fasPause; export const PENDING = farClock; export const PROFILE = fasUser; @@ -215,8 +205,6 @@ export const SORT = fasSort; export const SORT_ASCENDING = fasSortUp; export const SORT_DESCENDING = fasSortDown; export const SPINNER = fasSpinner; -export const SQUARE = farSquare; -export const SQUARE_MINUS = fasSquareMinus; export const STAR_FULL = fasStar; export const SUBTRACT = fasMinus; export const SYSTEM = fasLaptop; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 44115c787..8ebbd540b 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -2,8 +2,8 @@ export const AUTO_COMPLETE = 'autoComplete'; export const CAPTCHA = 'captcha'; export const CHECK = 'check'; export const DEVICE = 'device'; -export const KEY_VALUE_LIST = 'keyValueList'; export const PLAYLIST = 'playlist'; +export const KEY_VALUE_LIST = 'keyValueList'; export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const FLOAT = 'float'; @@ -15,12 +15,10 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const METADATA_PROFILE_SELECT = 'metadataProfileSelect'; export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect'; export const INDEXER_SELECT = 'indexerSelect'; -export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const SELECT = 'select'; export const SERIES_TYPE_SELECT = 'artistTypeSelect'; -export const ARTIST_TAG = 'artistTag'; export const DYNAMIC_SELECT = 'dynamicSelect'; export const TAG = 'tag'; export const TAG_SELECT = 'tagSelect'; @@ -34,8 +32,8 @@ export const all = [ CAPTCHA, CHECK, DEVICE, - KEY_VALUE_LIST, PLAYLIST, + KEY_VALUE_LIST, MONITOR_ALBUMS_SELECT, MONITOR_NEW_ITEMS_SELECT, FLOAT, @@ -50,7 +48,6 @@ export const all = [ DOWNLOAD_CLIENT_SELECT, ROOT_FOLDER_SELECT, SELECT, - ARTIST_TAG, DYNAMIC_SELECT, SERIES_TYPE_SELECT, TAG, diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.js index 6ac15f3bd..d7f85df5e 100644 --- a/frontend/src/Helpers/Props/sizes.js +++ b/frontend/src/Helpers/Props/sizes.js @@ -3,5 +3,5 @@ export const SMALL = 'small'; export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; -export const EXTRA_EXTRA_LARGE = 'extraExtraLarge'; -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE]; + +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js index f8c84e54b..91c284636 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js @@ -11,7 +11,6 @@ import Scroller from 'Components/Scroller/Scroller'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { scrollDirections } from 'Helpers/Props'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; import SelectAlbumRow from './SelectAlbumRow'; import styles from './SelectAlbumModalContent.css'; @@ -20,7 +19,6 @@ const columns = [ { name: 'title', label: () => translate('AlbumTitle'), - isSortable: true, isVisible: true }, { @@ -31,7 +29,6 @@ const columns = [ { name: 'releaseDate', label: () => translate('ReleaseDate'), - isSortable: true, isVisible: true }, { @@ -58,7 +55,7 @@ class SelectAlbumModalContent extends Component { // Listeners onFilterChange = ({ value }) => { - this.setState({ filter: value }); + this.setState({ filter: value.toLowerCase() }); }; // @@ -66,21 +63,14 @@ class SelectAlbumModalContent extends Component { render() { const { - isFetching, - isPopulated, - error, items, - sortKey, - sortDirection, - onSortPress, onAlbumSelect, - onModalClose + onModalClose, + isFetching, + ...otherProps } = this.props; const filter = this.state.filter; - const filterLower = filter.toLowerCase(); - - const errorMessage = getErrorMessage(error, 'Unable to load albums'); return ( @@ -92,34 +82,32 @@ class SelectAlbumModalContent extends Component { className={styles.modalBody} scrollDirection={scrollDirections.NONE} > + { + isFetching && + + } + + - {isFetching ? : null} - - {error ?
{errorMessage}
: null} - - - - {isPopulated && !!items.length ? ( + { { items.map((item) => { - return item.title.toLowerCase().includes(filterLower) ? + return item.title.toLowerCase().includes(filter) ? (
- ) : null} + }
@@ -148,13 +136,8 @@ class SelectAlbumModalContent extends Component { } SelectAlbumModalContent.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, - sortKey: PropTypes.string, - sortDirection: PropTypes.string, - onSortPress: PropTypes.func.isRequired, + isFetching: PropTypes.bool.isRequired, onAlbumSelect: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js index d09da0fca..12cd88e53 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js @@ -3,14 +3,18 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions'; -import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import { + clearInteractiveImportAlbums, + fetchInteractiveImportAlbums, + saveInteractiveImportItem, + setInteractiveImportAlbumsSort, + updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import SelectAlbumModalContent from './SelectAlbumModalContent'; function createMapStateToProps() { return createSelector( - createClientSideCollectionSelector('albumSelection'), + createClientSideCollectionSelector('interactiveImport.albums'), (albums) => { return albums; } @@ -18,9 +22,9 @@ function createMapStateToProps() { } const mapDispatchToProps = { - fetchAlbums, - setAlbumsSort, - clearAlbums, + fetchInteractiveImportAlbums, + setInteractiveImportAlbumsSort, + clearInteractiveImportAlbums, updateInteractiveImportItem, saveInteractiveImportItem }; @@ -35,20 +39,20 @@ class SelectAlbumModalContentConnector extends Component { artistId } = this.props; - this.props.fetchAlbums({ artistId }); + this.props.fetchInteractiveImportAlbums({ artistId }); } componentWillUnmount() { // This clears the albums for the queue and hides the queue // We'll need another place to store albums for manual import - this.props.clearAlbums(); + this.props.clearInteractiveImportAlbums(); } // // Listeners onSortPress = (sortKey, sortDirection) => { - this.props.setAlbumsSort({ sortKey, sortDirection }); + this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection }); }; onAlbumSelect = (albumId) => { @@ -78,7 +82,6 @@ class SelectAlbumModalContentConnector extends Component { return ( ); @@ -89,9 +92,9 @@ SelectAlbumModalContentConnector.propTypes = { ids: PropTypes.arrayOf(PropTypes.number).isRequired, artistId: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchAlbums: PropTypes.func.isRequired, - setAlbumsSort: PropTypes.func.isRequired, - clearAlbums: PropTypes.func.isRequired, + fetchInteractiveImportAlbums: PropTypes.func.isRequired, + setInteractiveImportAlbumsSort: PropTypes.func.isRequired, + clearInteractiveImportAlbums: PropTypes.func.isRequired, saveInteractiveImportItem: PropTypes.func.isRequired, updateInteractiveImportItem: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumRow.js b/frontend/src/InteractiveImport/Album/SelectAlbumRow.js index 6b50a1ce9..68d50fb8b 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumRow.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumRow.js @@ -44,9 +44,9 @@ class SelectAlbumRow extends Component { } = this.props; const { - trackCount = 0, - trackFileCount = 0, - totalTrackCount = 0 + trackCount, + trackFileCount, + totalTrackCount } = statistics; const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title; @@ -134,8 +134,7 @@ SelectAlbumRow.propTypes = { SelectAlbumRow.defaultProps = { statistics: { trackCount: 0, - trackFileCount: 0, - totalTrackCount: 0 + trackFileCount: 0 } }; diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js index 26fa7282a..d9301cdfa 100644 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js +++ b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js @@ -1,9 +1,10 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SelectInput from 'Components/Form/SelectInput'; +import FormInputGroup from 'Components/Form/FormInputGroup'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; +import { inputTypes } from 'Helpers/Props'; import shortenList from 'Utilities/String/shortenList'; import titleCase from 'Utilities/String/titleCase'; @@ -55,7 +56,8 @@ class SelectAlbumReleaseRow extends Component { if (name === 'release') { return ( - ({ key: r.id, diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js index 14c42a80b..2dceb979e 100644 --- a/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js @@ -29,7 +29,7 @@ class SelectArtistModalContent extends Component { // Listeners onFilterChange = ({ value }) => { - this.setState({ filter: value }); + this.setState({ filter: value.toLowerCase() }); }; // @@ -43,7 +43,6 @@ class SelectArtistModalContent extends Component { } = this.props; const filter = this.state.filter; - const filterLower = filter.toLowerCase(); return ( @@ -70,7 +69,7 @@ class SelectArtistModalContent extends Component { > { items.map((item) => { - return item.artistName.toLowerCase().includes(filterLower) ? + return item.artistName.toLowerCase().includes(filter) ? ( - - - ); - } -} - -SelectIndexerFlagsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectIndexerFlagsModal; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css deleted file mode 100644 index 72dfb1cb6..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css +++ /dev/null @@ -1,7 +0,0 @@ -.modalBody { - composes: modalBody from '~Components/Modal/ModalBody.css'; - - display: flex; - flex: 1 1 auto; - flex-direction: column; -} diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts deleted file mode 100644 index 3fc49a060..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'modalBody': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js deleted file mode 100644 index b30f76775..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds, scrollDirections } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './SelectIndexerFlagsModalContent.css'; - -class SelectIndexerFlagsModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const { - indexerFlags - } = props; - - this.state = { - indexerFlags - }; - } - - // - // Listeners - - onIndexerFlagsChange = ({ value }) => { - this.setState({ indexerFlags: value }); - }; - - onIndexerFlagsSelect = () => { - this.props.onIndexerFlagsSelect(this.state); - }; - - // - // Render - - render() { - const { - onModalClose - } = this.props; - - const { - indexerFlags - } = this.state; - - return ( - - - Manual Import - Set indexer Flags - - - - - - - {translate('IndexerFlags')} - - - - - - - - - - - - - - ); - } -} - -SelectIndexerFlagsModalContent.propTypes = { - indexerFlags: PropTypes.number.isRequired, - onIndexerFlagsSelect: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectIndexerFlagsModalContent; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js deleted file mode 100644 index 7a9af7353..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js +++ /dev/null @@ -1,54 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; -import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent'; - -const mapDispatchToProps = { - dispatchUpdateInteractiveImportItems: updateInteractiveImportItems, - dispatchSaveInteractiveImportItems: saveInteractiveImportItem -}; - -class SelectIndexerFlagsModalContentConnector extends Component { - - // - // Listeners - - onIndexerFlagsSelect = ({ indexerFlags }) => { - const { - ids, - dispatchUpdateInteractiveImportItems, - dispatchSaveInteractiveImportItems - } = this.props; - - dispatchUpdateInteractiveImportItems({ - ids, - indexerFlags - }); - - dispatchSaveInteractiveImportItems({ ids }); - - this.props.onModalClose(true); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -SelectIndexerFlagsModalContentConnector.propTypes = { - ids: PropTypes.arrayOf(PropTypes.number).isRequired, - dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, - dispatchSaveInteractiveImportItems: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css index 93d815c9c..573b16667 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css @@ -18,17 +18,12 @@ .leftButtons, .rightButtons { display: flex; + flex: 1 0 50%; flex-wrap: wrap; - min-width: 0; -} - -.leftButtons { - flex: 0 1 auto; } .rightButtons { justify-content: flex-end; - flex: 1 1 50%; } .importMode, @@ -36,7 +31,6 @@ composes: select from '~Components/Form/SelectInput.css'; margin-right: 10px; - max-width: 100%; width: auto; } @@ -49,12 +43,10 @@ .leftButtons, .rightButtons { flex-direction: column; - gap: 3px; } .leftButtons { align-items: flex-start; - max-width: fit-content; } .rightButtons { diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index d980e77ce..d1361a785 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -20,7 +20,6 @@ import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; -import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; @@ -31,7 +30,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected'; import InteractiveImportRow from './InteractiveImportRow'; import styles from './InteractiveImportModalContent.css'; -const COLUMNS = [ +const columns = [ { name: 'path', label: () => translate('Path'), @@ -80,21 +79,11 @@ const COLUMNS = [ isSortable: true, isVisible: true }, - { - name: 'indexerFlags', - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), - isSortable: true, - isVisible: true - }, { name: 'rejections', label: React.createElement(Icon, { name: icons.DANGER, - kind: kinds.DANGER, - title: () => translate('Rejections') + kind: kinds.DANGER }), isSortable: true, isVisible: true @@ -118,7 +107,6 @@ const ALBUM = 'album'; const ALBUM_RELEASE = 'albumRelease'; const RELEASE_GROUP = 'releaseGroup'; const QUALITY = 'quality'; -const INDEXER_FLAGS = 'indexerFlags'; const replaceExistingFilesOptions = { COMBINE: 'combine', @@ -313,21 +301,6 @@ class InteractiveImportModalContent extends Component { inconsistentAlbumReleases } = this.state; - const allColumns = _.cloneDeep(COLUMNS); - const columns = allColumns.map((column) => { - const showIndexerFlags = items.some((item) => item.indexerFlags); - - if (!showIndexerFlags) { - const indexerFlagsColumn = allColumns.find((c) => c.name === 'indexerFlags'); - - if (indexerFlagsColumn) { - indexerFlagsColumn.isVisible = false; - } - } - - return column; - }); - const selectedIds = this.getSelectedIds(); const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; const errorMessage = getErrorMessage(error, 'Unable to load manual import items'); @@ -337,8 +310,7 @@ class InteractiveImportModalContent extends Component { { key: ALBUM, value: translate('SelectAlbum') }, { key: ALBUM_RELEASE, value: translate('SelectAlbumRelease') }, { key: QUALITY, value: translate('SelectQuality') }, - { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }, - { key: INDEXER_FLAGS, value: translate('SelectIndexerFlags') } + { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') } ]; if (allowArtistChange) { @@ -461,7 +433,6 @@ class InteractiveImportModalContent extends Component { isSaving={isSaving} {...item} allowArtistChange={allowArtistChange} - columns={columns} onSelectedChange={this.onSelectedChange} onValidRowChange={this.onValidRowChange} /> @@ -576,13 +547,6 @@ class InteractiveImportModalContent extends Component { onModalClose={this.onSelectModalClose} /> - - 0 + quality ) { this.props.onSelectedChange({ id, value: true }); } @@ -135,10 +130,6 @@ class InteractiveImportRow extends Component { this.setState({ isSelectQualityModalOpen: true }); }; - onSelectIndexerFlagsPress = () => { - this.setState({ isSelectIndexerFlagsModalOpen: true }); - }; - onSelectArtistModalClose = (changed) => { this.setState({ isSelectArtistModalOpen: false }); this.selectRowAfterChange(changed); @@ -164,11 +155,6 @@ class InteractiveImportRow extends Component { this.selectRowAfterChange(changed); }; - onSelectIndexerFlagsModalClose = (changed) => { - this.setState({ isSelectIndexerFlagsModalOpen: false }); - this.selectRowAfterChange(changed); - }; - // // Render @@ -185,9 +171,7 @@ class InteractiveImportRow extends Component { releaseGroup, size, customFormats, - indexerFlags, rejections, - columns, isReprocessing, audioTags, additionalFile, @@ -200,8 +184,7 @@ class InteractiveImportRow extends Component { isSelectAlbumModalOpen, isSelectTrackModalOpen, isSelectReleaseGroupModalOpen, - isSelectQualityModalOpen, - isSelectIndexerFlagsModalOpen + isSelectQualityModalOpen } = this.state; const artistName = artist ? artist.artistName : ''; @@ -221,7 +204,6 @@ class InteractiveImportRow extends Component { const showTrackNumbersLoading = isReprocessing && !tracks.length; const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showQualityPlaceholder = isSelected && !quality; - const showIndexerFlagsPlaceholder = isSelected && !indexerFlags; const pathCellContents = (
@@ -237,8 +219,6 @@ class InteractiveImportRow extends Component { /> ) : pathCellContents; - const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false; - return ( - {isIndexerFlagsColumnVisible ? ( - - {showIndexerFlagsPlaceholder ? ( - - ) : ( - <> - {indexerFlags ? ( - } - title={translate('IndexerFlags')} - body={} - position={tooltipPositions.LEFT} - /> - ) : null} - - )} - - ) : null} - { rejections.length ? @@ -437,13 +395,6 @@ class InteractiveImportRow extends Component { real={quality ? quality.revision.real > 0 : false} onModalClose={this.onSelectQualityModalClose} /> - - ); } @@ -462,9 +413,7 @@ InteractiveImportRow.propTypes = { quality: PropTypes.object, size: PropTypes.number.isRequired, customFormats: PropTypes.arrayOf(PropTypes.object), - indexerFlags: PropTypes.number.isRequired, rejections: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, audioTags: PropTypes.object.isRequired, additionalFile: PropTypes.bool.isRequired, isReprocessing: PropTypes.bool, diff --git a/frontend/src/InteractiveImport/InteractiveImportModal.js b/frontend/src/InteractiveImport/InteractiveImportModal.js index 4d0e9f2f1..dda75152f 100644 --- a/frontend/src/InteractiveImport/InteractiveImportModal.js +++ b/frontend/src/InteractiveImport/InteractiveImportModal.js @@ -48,7 +48,7 @@ class InteractiveImportModal extends Component { return ( diff --git a/frontend/src/InteractiveImport/Track/SelectTrackRow.js b/frontend/src/InteractiveImport/Track/SelectTrackRow.js index d0a902576..ea621877d 100644 --- a/frontend/src/InteractiveImport/Track/SelectTrackRow.js +++ b/frontend/src/InteractiveImport/Track/SelectTrackRow.js @@ -51,11 +51,11 @@ class SelectTrackRow extends Component { iconKind = kinds.DEFAULT; iconTip = 'Track missing from library and no import selected.'; } else if (importSelected && hasFile) { - iconName = icons.FILE_IMPORT; + iconName = icons.FILEIMPORT; iconKind = kinds.WARNING; iconTip = 'Warning: Existing track will be replaced by download.'; } else if (importSelected && !hasFile) { - iconName = icons.FILE_IMPORT; + iconName = icons.FILEIMPORT; iconKind = kinds.DEFAULT; iconTip = 'Track missing from library and selected for import.'; } diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearch.js index 64d1ce730..6e74695b0 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ b/frontend/src/InteractiveSearch/InteractiveSearch.js @@ -65,15 +65,6 @@ const columns = [ isSortable: true, isVisible: true }, - { - name: 'indexerFlags', - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), - isSortable: true, - isVisible: true - }, { name: 'rejections', label: React.createElement(Icon, { diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index dad7242c8..c1079c792 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -22,10 +22,6 @@ text-align: center; } -.quality { - white-space: nowrap; -} - .customFormatScore { composes: cell from '~Components/Table/Cells/TableRowCell.css'; @@ -35,7 +31,6 @@ } .rejected, -.indexerFlags, .download { composes: cell from '~Components/Table/Cells/TableRowCell.css'; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts index bec6dcf78..ca01c5ee6 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts @@ -5,7 +5,6 @@ interface CssExports { 'customFormatScore': string; 'download': string; 'indexer': string; - 'indexerFlags': string; 'peers': string; 'protocol': string; 'quality': string; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.js b/frontend/src/InteractiveSearch/InteractiveSearchRow.js index a139f8085..ce32f12bd 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import AlbumFormats from 'Album/AlbumFormats'; -import IndexerFlags from 'Album/IndexerFlags'; import TrackQuality from 'Album/TrackQuality'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; @@ -49,12 +48,12 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { if (isGrabbing) { return ''; } else if (isGrabbed) { - return translate('AddedToDownloadQueue'); + return 'Added to downloaded queue'; } else if (grabError) { return grabError; } - return translate('AddToDownloadQueue'); + return 'Add to downloaded queue'; } class InteractiveSearchRow extends Component { @@ -130,7 +129,6 @@ class InteractiveSearchRow extends Component { quality, customFormatScore, customFormats, - indexerFlags = 0, rejections, downloadAllowed, isGrabbing, @@ -180,7 +178,7 @@ class InteractiveSearchRow extends Component { - + @@ -189,21 +187,10 @@ class InteractiveSearchRow extends Component { formatCustomFormatScore(customFormatScore, customFormats.length) } tooltip={} - position={tooltipPositions.LEFT} + position={tooltipPositions.BOTTOM} /> - - {indexerFlags ? ( - } - title={translate('IndexerFlags')} - body={} - position={tooltipPositions.LEFT} - /> - ) : null} - - { !!rejections.length && @@ -249,9 +236,7 @@ class InteractiveSearchRow extends Component { isOpen={this.state.isConfirmGrabModalOpen} kind={kinds.WARNING} title={translate('GrabRelease')} - message={translate('GrabReleaseUnknownArtistOrAlbumMessageText', { - title - })} + message={translate('GrabReleaseMessageText', [title])} confirmLabel={translate('Grab')} onConfirm={this.onGrabConfirm} onCancel={this.onGrabCancel} @@ -278,7 +263,6 @@ InteractiveSearchRow.propTypes = { quality: PropTypes.object.isRequired, customFormats: PropTypes.arrayOf(PropTypes.object), customFormatScore: PropTypes.number.isRequired, - indexerFlags: PropTypes.number.isRequired, rejections: PropTypes.arrayOf(PropTypes.string).isRequired, downloadAllowed: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired, @@ -291,7 +275,6 @@ InteractiveSearchRow.propTypes = { }; InteractiveSearchRow.defaultProps = { - indexerFlags: 0, rejections: [], isGrabbing: false, isGrabbed: false diff --git a/frontend/src/Parse/Parse.css b/frontend/src/Parse/Parse.css deleted file mode 100644 index 43536452c..000000000 --- a/frontend/src/Parse/Parse.css +++ /dev/null @@ -1,45 +0,0 @@ -.inputContainer { - display: flex; - margin-bottom: 10px; -} - -.inputIconContainer { - width: 58px; - height: 46px; - border: 1px solid var(--inputBorderColor); - border-right: none; - border-radius: 4px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - background-color: var(--inputIconContainerBackgroundColor); - text-align: center; - line-height: 46px; -} - -.input { - composes: input from '~Components/Form/TextInput.css'; - - height: 46px; - border-radius: 0; - font-size: 18px; -} - -.clearButton { - border: 1px solid var(--inputBorderColor); - border-left: none; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.message { - margin-top: 30px; - text-align: center; - font-weight: 300; - font-size: $largeFontSize; -} - -.helpText { - margin-bottom: 10px; - font-size: 24px; -} diff --git a/frontend/src/Parse/Parse.css.d.ts b/frontend/src/Parse/Parse.css.d.ts deleted file mode 100644 index 4a4def577..000000000 --- a/frontend/src/Parse/Parse.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'clearButton': string; - 'helpText': string; - 'input': string; - 'inputContainer': string; - 'inputIconContainer': string; - 'message': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Parse/Parse.tsx b/frontend/src/Parse/Parse.tsx deleted file mode 100644 index 15a0deb47..000000000 --- a/frontend/src/Parse/Parse.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import TextInput from 'Components/Form/TextInput'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons } from 'Helpers/Props'; -import { clear, fetch } from 'Store/Actions/parseActions'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import ParseResult from './ParseResult'; -import parseStateSelector from './parseStateSelector'; -import styles from './Parse.css'; - -function Parse() { - const { isFetching, error, item } = useSelector(parseStateSelector()); - - const [title, setTitle] = useState(''); - const dispatch = useDispatch(); - - const onInputChange = useCallback( - ({ value }: { value: string }) => { - const trimmedValue = value.trim(); - - setTitle(value); - - if (trimmedValue === '') { - dispatch(clear()); - } else { - dispatch(fetch({ title: trimmedValue })); - } - }, - [setTitle, dispatch] - ); - - const onClearPress = useCallback(() => { - setTitle(''); - dispatch(clear()); - }, [setTitle, dispatch]); - - useEffect( - () => { - return () => { - dispatch(clear()); - }; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return ( - - -
-
- -
- - - - -
- - {isFetching ? : null} - - {!isFetching && !!error ? ( -
-
- {translate('ParseModalErrorParsing')} -
-
{getErrorMessage(error)}
-
- ) : null} - - {!isFetching && title && !error && !item.parsedAlbumInfo ? ( -
- {translate('ParseModalUnableToParse')} -
- ) : null} - - {!isFetching && !error && item.parsedAlbumInfo ? ( - - ) : null} - - {title ? null : ( -
-
- {translate('ParseModalHelpText')} -
-
{translate('ParseModalHelpTextDetails')}
-
- )} -
-
- ); -} - -export default Parse; diff --git a/frontend/src/Parse/ParseModal.tsx b/frontend/src/Parse/ParseModal.tsx deleted file mode 100644 index 0ee455bf0..000000000 --- a/frontend/src/Parse/ParseModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ParseModalContent from './ParseModalContent'; - -interface ParseModalProps { - isOpen: boolean; - onModalClose: () => void; -} - -function ParseModal(props: ParseModalProps) { - const { isOpen, onModalClose } = props; - - return ( - - - - ); -} - -export default ParseModal; diff --git a/frontend/src/Parse/ParseModalContent.css b/frontend/src/Parse/ParseModalContent.css deleted file mode 100644 index 43536452c..000000000 --- a/frontend/src/Parse/ParseModalContent.css +++ /dev/null @@ -1,45 +0,0 @@ -.inputContainer { - display: flex; - margin-bottom: 10px; -} - -.inputIconContainer { - width: 58px; - height: 46px; - border: 1px solid var(--inputBorderColor); - border-right: none; - border-radius: 4px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - background-color: var(--inputIconContainerBackgroundColor); - text-align: center; - line-height: 46px; -} - -.input { - composes: input from '~Components/Form/TextInput.css'; - - height: 46px; - border-radius: 0; - font-size: 18px; -} - -.clearButton { - border: 1px solid var(--inputBorderColor); - border-left: none; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.message { - margin-top: 30px; - text-align: center; - font-weight: 300; - font-size: $largeFontSize; -} - -.helpText { - margin-bottom: 10px; - font-size: 24px; -} diff --git a/frontend/src/Parse/ParseModalContent.css.d.ts b/frontend/src/Parse/ParseModalContent.css.d.ts deleted file mode 100644 index 4a4def577..000000000 --- a/frontend/src/Parse/ParseModalContent.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'clearButton': string; - 'helpText': string; - 'input': string; - 'inputContainer': string; - 'inputIconContainer': string; - 'message': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Parse/ParseModalContent.tsx b/frontend/src/Parse/ParseModalContent.tsx deleted file mode 100644 index d5ae93759..000000000 --- a/frontend/src/Parse/ParseModalContent.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import TextInput from 'Components/Form/TextInput'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { icons } from 'Helpers/Props'; -import { clear, fetch } from 'Store/Actions/parseActions'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import ParseResult from './ParseResult'; -import parseStateSelector from './parseStateSelector'; -import styles from './ParseModalContent.css'; - -interface ParseModalContentProps { - onModalClose: () => void; -} - -function ParseModalContent(props: ParseModalContentProps) { - const { onModalClose } = props; - const { isFetching, error, item } = useSelector(parseStateSelector()); - - const [title, setTitle] = useState(''); - const dispatch = useDispatch(); - - const onInputChange = useCallback( - ({ value }: { value: string }) => { - const trimmedValue = value.trim(); - - setTitle(value); - - if (trimmedValue === '') { - dispatch(clear()); - } else { - dispatch(fetch({ title: trimmedValue })); - } - }, - [setTitle, dispatch] - ); - - const onClearPress = useCallback(() => { - setTitle(''); - dispatch(clear()); - }, [setTitle, dispatch]); - - useEffect( - () => { - return () => { - dispatch(clear()); - }; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return ( - - {translate('TestParsing')} - - -
-
- -
- - - - -
- - {isFetching ? : null} - - {!isFetching && !!error ? ( -
-
- {translate('ParseModalErrorParsing')} -
-
{getErrorMessage(error)}
-
- ) : null} - - {!isFetching && title && !error && !item.parsedAlbumInfo ? ( -
- {translate('ParseModalUnableToParse')} -
- ) : null} - - {!isFetching && !error && item.parsedAlbumInfo ? ( - - ) : null} - - {title ? null : ( -
-
- {translate('ParseModalHelpText')} -
-
{translate('ParseModalHelpTextDetails')}
-
- )} -
- - - - -
- ); -} - -export default ParseModalContent; diff --git a/frontend/src/Parse/ParseResult.css b/frontend/src/Parse/ParseResult.css deleted file mode 100644 index c49c4e3fa..000000000 --- a/frontend/src/Parse/ParseResult.css +++ /dev/null @@ -1,8 +0,0 @@ -.container { - display: flex; - flex-wrap: wrap; -} - -.column { - flex: 0 0 50%; -} diff --git a/frontend/src/Parse/ParseResult.css.d.ts b/frontend/src/Parse/ParseResult.css.d.ts deleted file mode 100644 index 653368e06..000000000 --- a/frontend/src/Parse/ParseResult.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'column': string; - 'container': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Parse/ParseResult.tsx b/frontend/src/Parse/ParseResult.tsx deleted file mode 100644 index 7e6c40d92..000000000 --- a/frontend/src/Parse/ParseResult.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import _ from 'lodash'; -import moment from 'moment'; -import React from 'react'; -import AlbumFormats from 'Album/AlbumFormats'; -import AlbumTitleLink from 'Album/AlbumTitleLink'; -import { ParseModel } from 'App/State/ParseAppState'; -import ArtistNameLink from 'Artist/ArtistNameLink'; -import FieldSet from 'Components/FieldSet'; -import translate from 'Utilities/String/translate'; -import ParseResultItem from './ParseResultItem'; -import styles from './ParseResult.css'; - -interface ParseResultProps { - item: ParseModel; -} - -function ParseResult(props: ParseResultProps) { - const { item } = props; - const { customFormats, customFormatScore, albums, parsedAlbumInfo, artist } = - item; - - const { - releaseTitle, - artistName, - albumTitle, - releaseGroup, - discography, - quality, - } = parsedAlbumInfo; - - const sortedAlbums = _.sortBy(albums, (item) => - moment(item.releaseDate).unix() - ); - - return ( -
-
- - - - - - - -
- -
-
-
- -
-
-
- -
-
-
- - 1 && !quality.revision.isRepack - ? translate('True') - : '-' - } - /> - - -
- -
- 1 ? quality.revision.version : '-' - } - /> - - -
-
-
- -
- - ) : ( - '-' - ) - } - /> - - - {sortedAlbums.map((album) => { - return ( -
- -
- ); - })} -
- ) : ( - '-' - ) - } - /> - - - ) : ( - '-' - ) - } - /> - - - -
- ); -} - -export default ParseResult; diff --git a/frontend/src/Parse/ParseResultItem.css b/frontend/src/Parse/ParseResultItem.css deleted file mode 100644 index 275fe7e1f..000000000 --- a/frontend/src/Parse/ParseResultItem.css +++ /dev/null @@ -1,21 +0,0 @@ -.item { - display: flex; -} - -.title { - margin-right: 20px; - width: 250px; - text-align: right; - font-weight: bold; -} - -@media (max-width: $breakpointSmall) { - .item { - display: block; - margin-bottom: 10px; - } - - .title { - text-align: left; - } -} diff --git a/frontend/src/Parse/ParseResultItem.tsx b/frontend/src/Parse/ParseResultItem.tsx deleted file mode 100644 index 661af448d..000000000 --- a/frontend/src/Parse/ParseResultItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { ReactNode } from 'react'; -import styles from './ParseResultItem.css'; - -interface ParseResultItemProps { - title: string; - data: string | number | ReactNode; -} - -function ParseResultItem(props: ParseResultItemProps) { - const { title, data } = props; - - return ( -
-
{title}
-
{data}
-
- ); -} - -export default ParseResultItem; diff --git a/frontend/src/Parse/ParseToolbarButton.tsx b/frontend/src/Parse/ParseToolbarButton.tsx deleted file mode 100644 index 43b8b959f..000000000 --- a/frontend/src/Parse/ParseToolbarButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { Fragment, useCallback, useState } from 'react'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import { icons } from 'Helpers/Props'; -import ParseModal from 'Parse/ParseModal'; -import translate from 'Utilities/String/translate'; - -function ParseToolbarButton() { - const [isParseModalOpen, setIsParseModalOpen] = useState(false); - - const onOpenParseModalPress = useCallback(() => { - setIsParseModalOpen(true); - }, [setIsParseModalOpen]); - - const onParseModalClose = useCallback(() => { - setIsParseModalOpen(false); - }, [setIsParseModalOpen]); - - return ( - - - - - - ); -} - -export default ParseToolbarButton; diff --git a/frontend/src/Parse/parseStateSelector.ts b/frontend/src/Parse/parseStateSelector.ts deleted file mode 100644 index 7abcfeca1..000000000 --- a/frontend/src/Parse/parseStateSelector.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import ParseAppState from 'App/State/ParseAppState'; - -export default function parseStateSelector() { - return createSelector( - (state: AppState) => state.parse, - (parse: ParseAppState) => { - return parse; - } - ); -} diff --git a/frontend/src/Quality/Quality.ts b/frontend/src/Quality/Quality.ts deleted file mode 100644 index 5be1475fc..000000000 --- a/frontend/src/Quality/Quality.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Revision { - version: number; - real: number; - isRepack: boolean; -} - -interface Quality { - id: number; - name: string; -} - -export interface QualityModel { - quality: Quality; - revision: Revision; -} - -export default Quality; diff --git a/frontend/src/Search/AddNewItem.js b/frontend/src/Search/AddNewItem.js index e335ef4c2..5ec065149 100644 --- a/frontend/src/Search/AddNewItem.js +++ b/frontend/src/Search/AddNewItem.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import TextInput from 'Components/Form/TextInput'; import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; @@ -131,8 +130,7 @@ class AddNewItem extends Component {
{translate('FailedLoadingSearchResults')}
- - {getErrorMessage(error)} +
{getErrorMessage(error)}
: null } diff --git a/frontend/src/Search/Album/AddNewAlbumModalContent.js b/frontend/src/Search/Album/AddNewAlbumModalContent.js index 2e461a5a0..5c2c70135 100644 --- a/frontend/src/Search/Album/AddNewAlbumModalContent.js +++ b/frontend/src/Search/Album/AddNewAlbumModalContent.js @@ -15,11 +15,26 @@ import styles from './AddNewAlbumModalContent.css'; class AddNewAlbumModalContent extends Component { + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + searchForNewAlbum: false + }; + } + // // Listeners + onSearchForNewAlbumChange = ({ value }) => { + this.setState({ searchForNewAlbum: value }); + }; + onAddAlbumPress = () => { - this.props.onAddAlbumPress(); + this.props.onAddAlbumPress(this.state.searchForNewAlbum); }; // @@ -32,12 +47,10 @@ class AddNewAlbumModalContent extends Component { disambiguation, overview, images, - searchForNewAlbum, isAdding, isExistingArtist, isSmallScreen, onModalClose, - onInputChange, ...otherProps } = this.props; @@ -92,7 +105,6 @@ class AddNewAlbumModalContent extends Component { } @@ -103,15 +115,15 @@ class AddNewAlbumModalContent extends Component { @@ -121,7 +133,7 @@ class AddNewAlbumModalContent extends Component { isSpinning={isAdding} onPress={this.onAddAlbumPress} > - {translate('AddAlbumWithTitle', { albumTitle })} + Add {albumTitle} @@ -137,11 +149,9 @@ AddNewAlbumModalContent.propTypes = { images: PropTypes.arrayOf(PropTypes.object).isRequired, isAdding: PropTypes.bool.isRequired, addError: PropTypes.object, - searchForNewAlbum: PropTypes.object.isRequired, isExistingArtist: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired, - onInputChange: PropTypes.func.isRequired, onAddAlbumPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Search/Album/AddNewAlbumModalContentConnector.js b/frontend/src/Search/Album/AddNewAlbumModalContentConnector.js index e315b49cd..dd7c03cab 100644 --- a/frontend/src/Search/Album/AddNewAlbumModalContentConnector.js +++ b/frontend/src/Search/Album/AddNewAlbumModalContentConnector.js @@ -83,7 +83,7 @@ class AddNewAlbumModalContentConnector extends Component { this.props.setAddDefault({ [name]: value }); }; - onAddAlbumPress = () => { + onAddAlbumPress = (searchForNewAlbum) => { const { foreignAlbumId, rootFolderPath, @@ -91,7 +91,6 @@ class AddNewAlbumModalContentConnector extends Component { monitorNewItems, qualityProfileId, metadataProfileId, - searchForNewAlbum, tags } = this.props; @@ -102,8 +101,8 @@ class AddNewAlbumModalContentConnector extends Component { monitorNewItems: monitorNewItems.value, qualityProfileId: qualityProfileId.value, metadataProfileId: metadataProfileId.value, - searchForNewAlbum: searchForNewAlbum.value, - tags: tags.value + tags: tags.value, + searchForNewAlbum }); }; @@ -130,7 +129,6 @@ AddNewAlbumModalContentConnector.propTypes = { qualityProfileId: PropTypes.object, metadataProfileId: PropTypes.object, noneMetadataProfileId: PropTypes.number.isRequired, - searchForNewAlbum: PropTypes.object.isRequired, tags: PropTypes.object.isRequired, onModalClose: PropTypes.func.isRequired, setAddDefault: PropTypes.func.isRequired, diff --git a/frontend/src/Search/Artist/AddNewArtistModalContent.js b/frontend/src/Search/Artist/AddNewArtistModalContent.js index 9719985f6..83e7898ed 100644 --- a/frontend/src/Search/Artist/AddNewArtistModalContent.js +++ b/frontend/src/Search/Artist/AddNewArtistModalContent.js @@ -15,11 +15,26 @@ import styles from './AddNewArtistModalContent.css'; class AddNewArtistModalContent extends Component { + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + searchForMissingAlbums: false + }; + } + // // Listeners + onSearchForMissingAlbumsChange = ({ value }) => { + this.setState({ searchForMissingAlbums: value }); + }; + onAddArtistPress = () => { - this.props.onAddArtistPress(); + this.props.onAddArtistPress(this.state.searchForMissingAlbums); }; // @@ -31,11 +46,9 @@ class AddNewArtistModalContent extends Component { disambiguation, overview, images, - searchForMissingAlbums, isAdding, isSmallScreen, onModalClose, - onInputChange, ...otherProps } = this.props; @@ -83,7 +96,6 @@ class AddNewArtistModalContent extends Component { @@ -94,15 +106,15 @@ class AddNewArtistModalContent extends Component { @@ -112,7 +124,7 @@ class AddNewArtistModalContent extends Component { isSpinning={isAdding} onPress={this.onAddArtistPress} > - {translate('AddArtistWithName', { artistName })} + Add {artistName} @@ -127,11 +139,9 @@ AddNewArtistModalContent.propTypes = { images: PropTypes.arrayOf(PropTypes.object).isRequired, isAdding: PropTypes.bool.isRequired, addError: PropTypes.object, - searchForMissingAlbums: PropTypes.object.isRequired, isSmallScreen: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired, - onInputChange: PropTypes.func.isRequired, onAddArtistPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Search/Artist/AddNewArtistModalContentConnector.js b/frontend/src/Search/Artist/AddNewArtistModalContentConnector.js index a33c4bbf7..1b50869e5 100644 --- a/frontend/src/Search/Artist/AddNewArtistModalContentConnector.js +++ b/frontend/src/Search/Artist/AddNewArtistModalContentConnector.js @@ -55,7 +55,7 @@ class AddNewArtistModalContentConnector extends Component { this.props.setAddDefault({ [name]: value }); }; - onAddArtistPress = () => { + onAddArtistPress = (searchForMissingAlbums) => { const { foreignArtistId, rootFolderPath, @@ -63,7 +63,6 @@ class AddNewArtistModalContentConnector extends Component { monitorNewItems, qualityProfileId, metadataProfileId, - searchForMissingAlbums, tags } = this.props; @@ -74,8 +73,8 @@ class AddNewArtistModalContentConnector extends Component { monitorNewItems: monitorNewItems.value, qualityProfileId: qualityProfileId.value, metadataProfileId: metadataProfileId.value, - searchForMissingAlbums: searchForMissingAlbums.value, - tags: tags.value + tags: tags.value, + searchForMissingAlbums }); }; @@ -100,7 +99,6 @@ AddNewArtistModalContentConnector.propTypes = { monitorNewItems: PropTypes.object.isRequired, qualityProfileId: PropTypes.object, metadataProfileId: PropTypes.object, - searchForMissingAlbums: PropTypes.object.isRequired, tags: PropTypes.object.isRequired, onModalClose: PropTypes.func.isRequired, setAddDefault: PropTypes.func.isRequired, diff --git a/frontend/src/Search/Artist/AddNewArtistSearchResult.js b/frontend/src/Search/Artist/AddNewArtistSearchResult.js index 83e287b5e..8ad5f6061 100644 --- a/frontend/src/Search/Artist/AddNewArtistSearchResult.js +++ b/frontend/src/Search/Artist/AddNewArtistSearchResult.js @@ -89,7 +89,7 @@ class AddNewArtistSearchResult extends Component { const linkProps = isExistingArtist ? { to: `/artist/${foreignArtistId}` } : { onPress: this.onPress }; - const endedString = artistType === 'Person' ? translate('Deceased') : translate('Inactive'); + const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive'; const height = calculateHeight(230, isSmallScreen); diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js new file mode 100644 index 000000000..342df29d2 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js @@ -0,0 +1,33 @@ +import React, { Component } from 'react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import translate from 'Utilities/String/translate'; +import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector'; + +class CustomFormatSettingsConnector extends Component { + + // + // Render + + render() { + return ( + + + + + + + + + + ); + } +} + +export default CustomFormatSettingsConnector; + diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx deleted file mode 100644 index 66c208f9a..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; -import ParseToolbarButton from 'Parse/ParseToolbarButton'; -import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; -import translate from 'Utilities/String/translate'; -import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector'; -import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton'; - -function CustomFormatSettingsPage() { - return ( - - - - - - - - - } - /> - - - {/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - - - - ); -} - -export default CustomFormatSettingsPage; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js index 0417d9b21..8e828620b 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import CustomFormats from './CustomFormats'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.customFormats', sortByProp('name')), + createSortedSectionSelector('settings.customFormats', sortByName), (customFormats) => customFormats ); } diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js index 3e79425cd..52b2f09f6 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { clearPendingChanges } from 'Store/Actions/baseActions'; import EditCustomFormatModal from './EditCustomFormatModal'; -import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector'; function mapStateToProps() { return {}; @@ -37,7 +36,6 @@ class EditCustomFormatModalConnector extends Component { } EditCustomFormatModalConnector.propTypes = { - ...EditCustomFormatModalContentConnector.propTypes, onModalClose: PropTypes.func.isRequired, clearPendingChanges: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css index 24830ef42..b7d3da255 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css @@ -25,8 +25,3 @@ border-radius: 4px; background-color: var(--cardCenterBackgroundColor); } - -.customFormats { - display: flex; - flex-wrap: wrap; -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts index 1aab6062e..1339caf02 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts @@ -3,7 +3,6 @@ interface CssExports { 'addSpecification': string; 'center': string; - 'customFormats': string; 'deleteButton': string; 'rightButtons': string; } diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js index 8b06deb4b..4d8f0fd4b 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import Card from 'Components/Card'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; @@ -151,11 +150,6 @@ class EditCustomFormatModalContent extends Component {
- -
- {translate('CustomFormatsSettingsTriggerInfo')} -
-
{ specifications.map((tag) => { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx deleted file mode 100644 index 3ff5cfa37..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageCustomFormatsEditModalContent from './ManageCustomFormatsEditModalContent'; - -interface ManageCustomFormatsEditModalProps { - isOpen: boolean; - customFormatIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -function ManageCustomFormatsEditModal( - props: ManageCustomFormatsEditModalProps -) { - const { isOpen, customFormatIds, onSavePress, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageCustomFormatsEditModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css deleted file mode 100644 index ea406894e..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.modalFooter { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - justify-content: space-between; -} - -.selected { - font-weight: bold; -} - -@media only screen and (max-width: $breakpointExtraSmall) { - .modalFooter { - flex-direction: column; - gap: 10px; - } -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts deleted file mode 100644 index cbf2d6328..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'modalFooter': string; - 'selected': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx deleted file mode 100644 index 25a2f85c2..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './ManageCustomFormatsEditModalContent.css'; - -interface SavePayload { - includeCustomFormatWhenRenaming?: boolean; -} - -interface ManageCustomFormatsEditModalContentProps { - customFormatIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -const NO_CHANGE = 'noChange'; - -const enableOptions = [ - { - key: NO_CHANGE, - get value() { - return translate('NoChange'); - }, - isDisabled: true, - }, - { - key: 'enabled', - get value() { - return translate('Enabled'); - }, - }, - { - key: 'disabled', - get value() { - return translate('Disabled'); - }, - }, -]; - -function ManageCustomFormatsEditModalContent( - props: ManageCustomFormatsEditModalContentProps -) { - const { customFormatIds, onSavePress, onModalClose } = props; - - const [includeCustomFormatWhenRenaming, setIncludeCustomFormatWhenRenaming] = - useState(NO_CHANGE); - - const save = useCallback(() => { - let hasChanges = false; - const payload: SavePayload = {}; - - if (includeCustomFormatWhenRenaming !== NO_CHANGE) { - hasChanges = true; - payload.includeCustomFormatWhenRenaming = - includeCustomFormatWhenRenaming === 'enabled'; - } - - if (hasChanges) { - onSavePress(payload); - } - - onModalClose(); - }, [includeCustomFormatWhenRenaming, onSavePress, onModalClose]); - - const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { - switch (name) { - case 'includeCustomFormatWhenRenaming': - setIncludeCustomFormatWhenRenaming(value); - break; - default: - console.warn( - `EditCustomFormatsModalContent Unknown Input: '${name}'` - ); - } - }, - [] - ); - - const selectedCount = customFormatIds.length; - - return ( - - {translate('EditSelectedCustomFormats')} - - - - {translate('IncludeCustomFormatWhenRenaming')} - - - - - - -
- {translate('CountCustomFormatsSelected', { - count: selectedCount, - })} -
- -
- - - -
-
-
- ); -} - -export default ManageCustomFormatsEditModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx deleted file mode 100644 index dd3456437..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageCustomFormatsModalContent from './ManageCustomFormatsModalContent'; - -interface ManageCustomFormatsModalProps { - isOpen: boolean; - onModalClose(): void; -} - -function ManageCustomFormatsModal(props: ManageCustomFormatsModalProps) { - const { isOpen, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageCustomFormatsModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css deleted file mode 100644 index 6ea04a0c8..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.leftButtons, -.rightButtons { - display: flex; - flex: 1 0 50%; - flex-wrap: wrap; -} - -.rightButtons { - justify-content: flex-end; -} - -.deleteButton { - composes: button from '~Components/Link/Button.css'; - - margin-right: 10px; -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts deleted file mode 100644 index 7b392fff9..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'deleteButton': string; - 'leftButtons': string; - 'rightButtons': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx deleted file mode 100644 index aabaf67c1..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { CustomFormatAppState } from 'App/State/SettingsAppState'; -import Alert from 'Components/Alert'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import Column from 'Components/Table/Column'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import useSelectState from 'Helpers/Hooks/useSelectState'; -import { kinds } from 'Helpers/Props'; -import { - bulkDeleteCustomFormats, - bulkEditCustomFormats, - setManageCustomFormatsSort, -} from 'Store/Actions/settingsActions'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import { SelectStateInputProps } from 'typings/props'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import ManageCustomFormatsEditModal from './Edit/ManageCustomFormatsEditModal'; -import ManageCustomFormatsModalRow from './ManageCustomFormatsModalRow'; -import styles from './ManageCustomFormatsModalContent.css'; - -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageCustomFormatsModalRow ->['onSelectedChange']; - -const COLUMNS: Column[] = [ - { - name: 'name', - label: () => translate('Name'), - isSortable: true, - isVisible: true, - }, - { - name: 'includeCustomFormatWhenRenaming', - label: () => translate('IncludeCustomFormatWhenRenaming'), - isSortable: true, - isVisible: true, - }, - { - name: 'actions', - label: '', - isVisible: true, - }, -]; - -interface ManageCustomFormatsModalContentProps { - onModalClose(): void; -} - -function ManageCustomFormatsModalContent( - props: ManageCustomFormatsModalContentProps -) { - const { onModalClose } = props; - - const { - isFetching, - isPopulated, - isDeleting, - isSaving, - error, - items, - sortKey, - sortDirection, - }: CustomFormatAppState = useSelector( - createClientSideCollectionSelector('settings.customFormats') - ); - const dispatch = useDispatch(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - - const [selectState, setSelectState] = useSelectState(); - - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; - - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageCustomFormatsSort({ sortKey: value })); - }, - [dispatch] - ); - - const onDeletePress = useCallback(() => { - setIsDeleteModalOpen(true); - }, [setIsDeleteModalOpen]); - - const onDeleteModalClose = useCallback(() => { - setIsDeleteModalOpen(false); - }, [setIsDeleteModalOpen]); - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteCustomFormats({ ids: selectedIds })); - setIsDeleteModalOpen(false); - }, [selectedIds, dispatch]); - - const onSavePress = useCallback( - (payload: object) => { - setIsEditModalOpen(false); - - dispatch( - bulkEditCustomFormats({ - ids: selectedIds, - ...payload, - }) - ); - }, - [selectedIds, dispatch] - ); - - const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); - }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] - ); - - const errorMessage = getErrorMessage(error, 'Unable to load custom formats.'); - const anySelected = selectedCount > 0; - - return ( - - {translate('ManageCustomFormats')} - - {isFetching ? : null} - - {error ?
{errorMessage}
: null} - - {isPopulated && !error && !items.length ? ( - {translate('NoCustomFormatsFound')} - ) : null} - - {isPopulated && !!items.length && !isFetching && !isFetching ? ( - - - {items.map((item) => { - return ( - - ); - })} - -
- ) : null} -
- - -
- - {translate('Delete')} - - - - {translate('Edit')} - -
- - -
- - - - -
- ); -} - -export default ManageCustomFormatsModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css deleted file mode 100644 index 355c70378..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css +++ /dev/null @@ -1,12 +0,0 @@ -.name, -.includeCustomFormatWhenRenaming { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - word-break: break-all; -} - -.actions { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 40px; -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts deleted file mode 100644 index d1719edd8..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'actions': string; - 'includeCustomFormatWhenRenaming': string; - 'name': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx deleted file mode 100644 index 57bb7fda0..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import Column from 'Components/Table/Column'; -import TableRow from 'Components/Table/TableRow'; -import { icons } from 'Helpers/Props'; -import { deleteCustomFormat } from 'Store/Actions/settingsActions'; -import { SelectStateInputProps } from 'typings/props'; -import translate from 'Utilities/String/translate'; -import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector'; -import styles from './ManageCustomFormatsModalRow.css'; - -interface ManageCustomFormatsModalRowProps { - id: number; - name: string; - includeCustomFormatWhenRenaming: boolean; - columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; -} - -function isDeletingSelector() { - return createSelector( - (state: AppState) => state.settings.customFormats.isDeleting, - (isDeleting) => { - return isDeleting; - } - ); -} - -function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { - const { - id, - isSelected, - name, - includeCustomFormatWhenRenaming, - onSelectedChange, - } = props; - - const dispatch = useDispatch(); - const isDeleting = useSelector(isDeletingSelector()); - - const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] = - useState(false); - - const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] = - useState(false); - - const handlelectedChange = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, - }); - }, - [onSelectedChange] - ); - - const handleEditCustomFormatModalOpen = useCallback(() => { - setIsEditCustomFormatModalOpen(true); - }, [setIsEditCustomFormatModalOpen]); - - const handleEditCustomFormatModalClose = useCallback(() => { - setIsEditCustomFormatModalOpen(false); - }, [setIsEditCustomFormatModalOpen]); - - const handleDeleteCustomFormatPress = useCallback(() => { - setIsEditCustomFormatModalOpen(false); - setIsDeleteCustomFormatModalOpen(true); - }, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]); - - const handleDeleteCustomFormatModalClose = useCallback(() => { - setIsDeleteCustomFormatModalOpen(false); - }, [setIsDeleteCustomFormatModalOpen]); - - const handleConfirmDeleteCustomFormat = useCallback(() => { - dispatch(deleteCustomFormat({ id })); - }, [id, dispatch]); - - return ( - - - - {name} - - - {includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')} - - - - - - - - - - - ); -} - -export default ManageCustomFormatsModalRow; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx deleted file mode 100644 index 91f41dc44..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import ManageCustomFormatsModal from './ManageCustomFormatsModal'; - -function ManageCustomFormatsToolbarButton() { - const [isManageModalOpen, openManageModal, closeManageModal] = - useModalOpenState(false); - - return ( - <> - - - - - ); -} - -export default ManageCustomFormatsToolbarButton; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js index 19aae4694..d55bbcdf9 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js @@ -7,8 +7,8 @@ import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; @@ -49,16 +49,15 @@ function EditSpecificationModalContent(props) { {...otherProps} > { - fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) && + fields && fields.some((x) => x.label === 'Regular Expression') &&
- +
\\^$.|?*+()[{ have special meanings and need escaping with a \\' }} /> + {'More details'} {'Here'}
- -
-
- + {'Regular expressions can be tested '} + Here
} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index 0dc410fcb..d9e543469 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByProp('name')), + createSortedSectionSelector('settings.downloadClients', sortByName), createTagsSelector(), (downloadClients, tagList) => { return { diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index 8d7d994f7..7e43dea12 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -15,7 +15,6 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import styles from './EditDownloadClientModalContent.css'; @@ -38,7 +37,6 @@ class EditDownloadClientModalContent extends Component { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteDownloadClientPress, ...otherProps } = this.props; @@ -141,7 +139,7 @@ class EditDownloadClientModalContent extends Component { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -76,7 +65,6 @@ class EditDownloadClientModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -94,7 +82,6 @@ EditDownloadClientModalContentConnector.propTypes = { setDownloadClientFieldValue: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired, testDownloadClient: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index 7599cb9b0..18ae5170a 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css index 6ea04a0c8..c106388ab 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} +} \ No newline at end of file diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index b2c1208cb..12f9efedb 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -10,7 +10,6 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; @@ -18,7 +17,6 @@ import { kinds } from 'Helpers/Props'; import { bulkDeleteDownloadClients, bulkEditDownloadClients, - setManageDownloadClientsSort, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { SelectStateInputProps } from 'typings/props'; @@ -35,7 +33,7 @@ type OnSelectedChangeCallback = React.ComponentProps< typeof ManageDownloadClientsModalRow >['onSelectedChange']; -const COLUMNS: Column[] = [ +const COLUMNS = [ { name: 'name', label: () => translate('Name'), @@ -96,8 +94,6 @@ function ManageDownloadClientsModalContent( isSaving, error, items, - sortKey, - sortDirection, }: DownloadClientAppState = useSelector( createClientSideCollectionSelector('settings.downloadClients') ); @@ -118,13 +114,6 @@ function ManageDownloadClientsModalContent( const selectedCount = selectedIds.length; - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageDownloadClientsSort({ sortKey: value })); - }, - [dispatch] - ); - const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); }, [setIsDeleteModalOpen]); @@ -218,9 +207,9 @@ function ManageDownloadClientsModalContent( {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isPopulated && !error && !items.length && ( {translate('NoDownloadClientsFound')} - ) : null} + )} {isPopulated && !!items.length && !isFetching && !isFetching ? ( {items.map((item) => { diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js index 262143590..48d8f3410 100644 --- a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js @@ -61,12 +61,10 @@ function DownloadClientOptions(props) { legend={translate('FailedDownloadHandling')} >
- - {translate('AutoRedownloadFailed')} + + + {translate('Redownload')} + - - { - settings.autoRedownloadFailed.value ? - - {translate('AutoRedownloadFailedFromInteractiveSearch')} - - - : - null - } - {translate('RemoveDownloadsAlert')} diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js index 39340c856..91e7287ee 100644 --- a/frontend/src/Settings/General/GeneralSettings.js +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -156,7 +156,6 @@ class GeneralSettings extends Component { /> diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.js index 043867853..93918a3d2 100644 --- a/frontend/src/Settings/General/LoggingSettings.js +++ b/frontend/src/Settings/General/LoggingSettings.js @@ -15,14 +15,12 @@ const logLevelOptions = [ function LoggingSettings(props) { const { - advancedSettings, settings, onInputChange } = props; const { - logLevel, - logSizeLimit + logLevel } = settings; return ( @@ -41,30 +39,11 @@ function LoggingSettings(props) { {...logLevel} /> - - - {translate('LogSizeLimit')} - - - ); } LoggingSettings.propTypes = { - advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/General/SecuritySettings.js b/frontend/src/Settings/General/SecuritySettings.js index d48d5abf6..bb20a9305 100644 --- a/frontend/src/Settings/General/SecuritySettings.js +++ b/frontend/src/Settings/General/SecuritySettings.js @@ -11,69 +11,16 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import { icons, inputTypes, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; -export const authenticationMethodOptions = [ - { - key: 'none', - get value() { - return translate('None'); - }, - isDisabled: true - }, - { - key: 'external', - get value() { - return translate('External'); - }, - isHidden: true - }, - { - key: 'basic', - get value() { - return translate('AuthBasic'); - } - }, - { - key: 'forms', - get value() { - return translate('AuthForm'); - } - } -]; - -export const authenticationRequiredOptions = [ - { - key: 'enabled', - get value() { - return translate('Enabled'); - } - }, - { - key: 'disabledForLocalAddresses', - get value() { - return translate('DisabledForLocalAddresses'); - } - } +const authenticationMethodOptions = [ + { key: 'none', value: 'None' }, + { key: 'basic', value: 'Basic (Browser Popup)' }, + { key: 'forms', value: 'Forms (Login Page)' } ]; const certificateValidationOptions = [ - { - key: 'enabled', - get value() { - return translate('Enabled'); - } - }, - { - key: 'disabledForLocalAddresses', - get value() { - return translate('DisabledForLocalAddresses'); - } - }, - { - key: 'disabled', - get value() { - return translate('Disabled'); - } - } + { key: 'enabled', value: 'Enabled' }, + { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }, + { key: 'disabled', value: 'Disabled' } ]; class SecuritySettings extends Component { @@ -121,10 +68,8 @@ class SecuritySettings extends Component { const { authenticationMethod, - authenticationRequired, username, password, - passwordConfirmation, apiKey, certificateValidation } = settings; @@ -143,31 +88,13 @@ class SecuritySettings extends Component { name="authenticationMethod" values={authenticationMethodOptions} helpText={translate('AuthenticationMethodHelpText')} - helpTextWarning={translate('AuthenticationRequiredWarning')} onChange={onInputChange} {...authenticationMethod} /> { - authenticationEnabled ? - - {translate('AuthenticationRequired')} - - - : - null - } - - { - authenticationEnabled ? + authenticationEnabled && {translate('Username')} @@ -179,12 +106,11 @@ class SecuritySettings extends Component { onChange={onInputChange} {...username} /> - : - null + } { - authenticationEnabled ? + authenticationEnabled && {translate('Password')} @@ -196,23 +122,7 @@ class SecuritySettings extends Component { onChange={onInputChange} {...password} /> - : - null - } - - { - authenticationEnabled ? - - {translate('PasswordConfirmation')} - - - : - null + } diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index a151423e5..0d7c0cbba 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -18,6 +18,7 @@ function UpdateSettings(props) { const { advancedSettings, settings, + isWindows, packageUpdateMechanism, onInputChange } = props; @@ -43,10 +44,10 @@ function UpdateSettings(props) { value: titleCase(packageUpdateMechanism) }); } else { - updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') }); + updateOptions.push({ key: 'builtIn', value: 'Built-In' }); } - updateOptions.push({ key: 'script', value: translate('Script') }); + updateOptions.push({ key: 'script', value: 'Script' }); return (
@@ -68,59 +69,62 @@ function UpdateSettings(props) { /> -
- - {translate('Automatic')} + { + !isWindows && +
+ + {translate('Automatic')} - - + + - - {translate('Mechanism')} - - - - - { - updateMechanism.value === 'script' && - {translate('ScriptPath')} + {translate('Mechanism')} - } -
+ + { + updateMechanism.value === 'script' && + + {translate('ScriptPath')} + + + + } +
+ }
); } diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css index 7c603a8b5..349ebc925 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css @@ -8,13 +8,11 @@ } .artistName { - @add-mixin truncate; - - flex: 0 1 600px; + flex: 0 0 300px; } .foreignId { - flex: 0 0 290px; + flex: 0 0 400px; } .actions { diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css index 0e3f43824..99e1c1e99 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css @@ -4,12 +4,12 @@ font-weight: bold; } -.name { - flex: 0 1 600px; +.host { + flex: 0 0 300px; } -.foreignId { - flex: 0 0 290px; +.path { + flex: 0 0 400px; } .addImportListExclusion { diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts index 397310d60..bf7ee773c 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts @@ -3,9 +3,9 @@ interface CssExports { 'addButton': string; 'addImportListExclusion': string; - 'foreignId': string; + 'host': string; 'importListExclusionsHeader': string; - 'name': string; + 'path': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js index 12e3feabc..fa1f5370a 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js @@ -51,10 +51,8 @@ class ImportListExclusions extends Component { {...otherProps} >
-
- {translate('Name')} -
-
+
{translate('Name')}
+
{translate('ForeignId')}
diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index d50fb2385..ed9582e91 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -20,7 +20,6 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import Popover from 'Components/Tooltip/Popover'; import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import translate from 'Utilities/String/translate'; import styles from './EditImportListModalContent.css'; @@ -67,7 +66,6 @@ function EditImportListModalContent(props) { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteImportListPress, showMetadataProfile, ...otherProps @@ -292,7 +290,7 @@ function EditImportListModalContent(props) { @@ -335,12 +333,6 @@ function EditImportListModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -78,7 +67,6 @@ class EditImportListModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -96,7 +84,6 @@ EditImportListModalContentConnector.propTypes = { setImportListFieldValue: PropTypes.func.isRequired, saveImportList: PropTypes.func.isRequired, testImportList: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js index 5eb47068d..5c6bad8e7 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteImportList, fetchImportLists, fetchRootFolders } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import ImportLists from './ImportLists'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.importLists', sortByProp('name')), + createSortedSectionSelector('settings.importLists', sortByName), (importLists) => importLists ); } diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx index 82f7d309c..5a651ba28 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx @@ -31,7 +31,7 @@ const autoAddOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css index 6ea04a0c8..c106388ab 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} +} \ No newline at end of file diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index 4fee485c9..60619c662 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -198,9 +198,9 @@ function ManageImportListsModalContent( {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isPopulated && !error && !items.length && ( {translate('NoImportListsFound')} - ) : null} + )} {isPopulated && !!items.length && !isFetching && !isFetching ? (
- {qualityProfile?.name ?? translate('None')} + {qualityProfile?.name ?? 'None'} @@ -72,7 +71,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) { - {enableAutomaticAdd ? translate('Yes') : translate('No')} + {enableAutomaticAdd ? 'Yes' : 'No'} diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js index bda52ac42..55235c9da 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -160,7 +160,7 @@ function EditIndexerModalContent(props) { { return { diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx index 69ad5a988..f9b051986 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css index 6ea04a0c8..c106388ab 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} +} \ No newline at end of file diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 997d1b566..37c4a3153 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -10,7 +10,6 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; @@ -18,7 +17,6 @@ import { kinds } from 'Helpers/Props'; import { bulkDeleteIndexers, bulkEditIndexers, - setManageIndexersSort, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { SelectStateInputProps } from 'typings/props'; @@ -35,7 +33,7 @@ type OnSelectedChangeCallback = React.ComponentProps< typeof ManageIndexersModalRow >['onSelectedChange']; -const COLUMNS: Column[] = [ +const COLUMNS = [ { name: 'name', label: () => translate('Name'), @@ -94,8 +92,6 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { isSaving, error, items, - sortKey, - sortDirection, }: IndexerAppState = useSelector( createClientSideCollectionSelector('settings.indexers') ); @@ -116,13 +112,6 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { const selectedCount = selectedIds.length; - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageIndexersSort({ sortKey: value })); - }, - [dispatch] - ); - const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); }, [setIsDeleteModalOpen]); @@ -213,9 +202,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isPopulated && !error && !items.length && ( {translate('NoIndexersFound')} - ) : null} + )} {isPopulated && !!items.length && !isFetching && !isFetching ? (
{items.map((item) => { diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index 627263fff..d38bc7224 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -191,21 +191,26 @@ class MediaManagement extends Component {
- - {translate('SkipFreeSpaceCheck')} + { + !isWindows && + + + {translate('SkipFreeSpaceCheck')} + - - + + + } { - settings.importExtraFiles.value ? + settings.importExtraFiles.value && - {translate('ImportExtraFiles')} + + {translate('ImportExtraFiles')} + - : null + }
} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js index 8f96fabfb..914e8d4c5 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -14,11 +15,11 @@ function createMapStateToProps() { (state) => state.settings.advancedSettings, (state) => state.settings.namingExamples, createSettingsSectionSelector(SECTION), - (advancedSettings, namingExamples, sectionSettings) => { + (advancedSettings, examples, sectionSettings) => { return { advancedSettings, - examples: namingExamples.item, - examplesPopulated: namingExamples.isPopulated, + examples: examples.item, + examplesPopulated: !_.isEmpty(examples.item), ...sectionSettings }; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index dec15893f..35244e64e 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -15,51 +15,16 @@ import NamingOption from './NamingOption'; import styles from './NamingModal.css'; const separatorOptions = [ - { - key: ' ', - get value() { - return `${translate('Space')} ( )`; - } - }, - { - key: '.', - get value() { - return `${translate('Period')} (.)`; - } - }, - { - key: '_', - get value() { - return `${translate('Underscore')} (_)`; - } - }, - { - key: '-', - get value() { - return `${translate('Dash')} (-)`; - } - } + { key: ' ', value: 'Space ( )' }, + { key: '.', value: 'Period (.)' }, + { key: '_', value: 'Underscore (_)' }, + { key: '-', value: 'Dash (-)' } ]; const caseOptions = [ - { - key: 'title', - get value() { - return translate('DefaultCase'); - } - }, - { - key: 'lower', - get value() { - return translate('Lowercase'); - } - }, - { - key: 'upper', - get value() { - return translate('Uppercase'); - } - } + { key: 'title', value: 'Default Case' }, + { key: 'lower', value: 'Lowercase' }, + { key: 'upper', value: 'Uppercase' } ]; const fileNameTokens = [ @@ -75,23 +40,33 @@ const fileNameTokens = [ const artistTokens = [ { token: '{Artist Name}', example: 'Artist Name' }, - { token: '{Artist CleanName}', example: 'Artist Name' }, + { token: '{Artist NameThe}', example: 'Artist Name, The' }, - { token: '{Artist CleanNameThe}', example: 'Artist Name, The' }, + { token: '{Artist NameFirstCharacter}', example: 'A' }, + + { token: '{Artist CleanName}', example: 'Artist Name' }, + { token: '{Artist Disambiguation}', example: 'Disambiguation' }, + { token: '{Artist Genre}', example: 'Pop' }, + { token: '{Artist MbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' } ]; const albumTokens = [ { token: '{Album Title}', example: 'Album Title' }, - { token: '{Album CleanTitle}', example: 'Album Title' }, + { token: '{Album TitleThe}', example: 'Album Title, The' }, - { token: '{Album CleanTitleThe}', example: 'Album Title, The' }, + + { token: '{Album CleanTitle}', example: 'Album Title' }, + { token: '{Album Type}', example: 'Album Type' }, + { token: '{Album Disambiguation}', example: 'Disambiguation' }, + { token: '{Album Genre}', example: 'Rock' }, + { token: '{Album MbId}', example: '082c6aff-a7cc-36e0-a960-35a578ecd937' } ]; @@ -121,9 +96,8 @@ const trackTitleTokens = [ const trackArtistTokens = [ { token: '{Track ArtistName}', example: 'Artist Name' }, - { token: '{Track ArtistCleanName}', example: 'Artist Name' }, { token: '{Track ArtistNameThe}', example: 'Artist Name, The' }, - { token: '{Track ArtistCleanNameThe}', example: 'Artist Name, The' }, + { token: '{Track ArtistCleanName}', example: 'Artist Name' }, { token: '{Track ArtistMbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' } ]; @@ -239,7 +213,7 @@ class NamingModal extends Component { > - {translate('FileNameTokens')} + File Name Tokens @@ -578,7 +552,7 @@ class NamingModal extends Component { onSelectionChange={this.onInputSelectionChange} /> diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index 204c93d0e..b692362fb 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -1,6 +1,6 @@ .option { display: flex; - align-items: stretch; + align-items: center; flex-wrap: wrap; margin: 3px; border: 1px solid var(--borderColor); @@ -17,7 +17,7 @@ } .small { - width: 490px; + width: 460px; } .large { @@ -26,7 +26,7 @@ .token { flex: 0 0 50%; - padding: 6px; + padding: 6px 16px; background-color: var(--popoverTitleBackgroundColor); font-family: $monoSpaceFontFamily; } @@ -34,9 +34,9 @@ .example { display: flex; align-items: center; - justify-content: space-between; + align-self: stretch; flex: 0 0 50%; - padding: 6px; + padding: 6px 16px; background-color: var(--popoverBodyBackgroundColor); .footNote { diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js index dc91e4622..20cefc53f 100644 --- a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js +++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js @@ -75,12 +75,12 @@ class RootFolder extends Component { {path} - } { - supportsOnUpgrade && onReleaseImport && onUpgrade ? + supportsOnUpgrade && onReleaseImport && onUpgrade && : - null + } { - supportsOnRename && onRename ? + supportsOnRename && onRename && : - null + } { - supportsOnTrackRetag && onTrackRetag ? + supportsOnTrackRetag && onTrackRetag && : - null + } { - supportsOnArtistAdd && onArtistAdd ? - : - null - } - - { - supportsOnArtistDelete && onArtistDelete ? - : - null - } - - { - supportsOnAlbumDelete && onAlbumDelete ? + supportsOnAlbumDelete && onAlbumDelete && : - null + } { - supportsOnHealthIssue && onHealthIssue ? + supportsOnArtistDelete && onArtistDelete && + + } + + { + supportsOnHealthIssue && onHealthIssue && : - null + } { @@ -177,38 +159,35 @@ class Notification extends Component { } { - supportsOnDownloadFailure && onDownloadFailure ? + supportsOnDownloadFailure && onDownloadFailure && : - null + } { - supportsOnImportFailure && onImportFailure ? + supportsOnImportFailure && onImportFailure && : - null + } { - supportsOnApplicationUpdate && onApplicationUpdate ? + supportsOnApplicationUpdate && onApplicationUpdate && : - null + } { - !onGrab && !onReleaseImport && !onRename && !onTrackRetag && !onArtistAdd && !onArtistDelete && !onAlbumDelete && !onHealthIssue && !onHealthRestored && !onDownloadFailure && !onImportFailure && !onApplicationUpdate ? - : - null + !onGrab && !onReleaseImport && !onRename && !onTrackRetag && !onAlbumDelete && !onArtistDelete && + !onHealthIssue && !onHealthRestored && !onDownloadFailure && !onImportFailure && !onApplicationUpdate && + } -
- -
- -
- -
-
+
+ +
+
{ return { diff --git a/frontend/src/Settings/PendingChangesModal.js b/frontend/src/Settings/PendingChangesModal.js index 900022ca1..478c7cafa 100644 --- a/frontend/src/Settings/PendingChangesModal.js +++ b/frontend/src/Settings/PendingChangesModal.js @@ -14,17 +14,12 @@ function PendingChangesModal(props) { isOpen, onConfirm, onCancel, - bindShortcut, - unbindShortcut + bindShortcut } = props; useEffect(() => { - if (isOpen) { - bindShortcut('enter', onConfirm); - - return () => unbindShortcut('enter', onConfirm); - } - }, [bindShortcut, unbindShortcut, isOpen, onConfirm]); + bindShortcut('enter', onConfirm); + }, [bindShortcut, onConfirm]); return ( - {translate('AddDelayProfileError')} - : +
+ {translate('UnableToAddANewQualityProfilePleaseTryAgain')} +
: null } @@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) { { id === 1 ? - {translate('DefaultDelayProfileArtist')} + {translate('DefaultDelayProfileHelpText')} : @@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) { type={inputTypes.TAG} name="tags" {...tags} - helpText={translate('DelayProfileArtistTagsHelpText')} + helpText={translate('TagsHelpText')} onChange={onInputChange} /> diff --git a/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js index 5e719517f..d39303ecc 100644 --- a/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js +++ b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js @@ -5,7 +5,7 @@ import FieldSet from 'Components/FieldSet'; import Icon from 'Components/Icon'; import PageSectionContent from 'Components/Page/PageSectionContent'; import { icons, metadataProfileNames } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector'; import MetadataProfile from './MetadataProfile'; @@ -59,20 +59,17 @@ class MetadataProfiles extends Component { >
{ - items - .filter((item) => item.name !== metadataProfileNames.NONE) - .sort(sortByProp('name')) - .map((item) => { - return ( - - ); - }) + items.filter((item) => item.name !== metadataProfileNames.NONE).sort(sortByName).map((item) => { + return ( + + ); + }) } b.name ? 1 : -1; }).map((x) => items[x.format]); } diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js index 4cb318463..581882ffd 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import QualityProfiles from './QualityProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), + createSortedSectionSelector('settings.qualityProfiles', sortByName), (qualityProfiles) => qualityProfiles ); } diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js index e1c695c42..c6c297c81 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js @@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) { diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css b/frontend/src/Settings/Quality/Definition/QualityDefinition.css index 860333725..f9d303498 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.css +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css @@ -24,19 +24,19 @@ height: 20px; } -.track { +.bar { top: 9px; margin: 0 5px; height: 3px; background-color: var(--sliderAccentColor); box-shadow: 0 0 0 #000; - &:nth-child(3n + 1) { + &:nth-child(odd) { background-color: #ddd; } } -.thumb { +.handle { top: 1px; z-index: 0 !important; width: 18px; @@ -56,7 +56,7 @@ .kilobitsPerSecond { display: flex; justify-content: space-between; - flex: 0 0 400px; + flex: 0 0 250px; } .sizeInput { diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts b/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts index 9c9e8393a..2b92fb212 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts @@ -1,6 +1,8 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'bar': string; + 'handle': string; 'kilobitsPerSecond': string; 'quality': string; 'qualityDefinition': string; @@ -8,9 +10,7 @@ interface CssExports { 'sizeLimit': string; 'sizes': string; 'slider': string; - 'thumb': string; 'title': string; - 'track': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js index 48251abfb..a289631bc 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.js +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js @@ -50,45 +50,21 @@ class QualityDefinition extends Component { this.state = { sliderMinSize: getSliderValue(props.minSize, slider.min), - sliderMaxSize: getSliderValue(props.maxSize, slider.max), - sliderPreferredSize: getSliderValue(props.preferredSize, (slider.max - 3)) + sliderMaxSize: getSliderValue(props.maxSize, slider.max) }; } - // - // Control - - trackRenderer(props, state) { - return ( -
- ); - } - - thumbRenderer(props, state) { - return ( -
- ); - } - // // Listeners - onSliderChange = ([sliderMinSize, sliderPreferredSize, sliderMaxSize]) => { + onSliderChange = ([sliderMinSize, sliderMaxSize]) => { this.setState({ sliderMinSize, - sliderMaxSize, - sliderPreferredSize + sliderMaxSize }); this.props.onSizeChange({ minSize: roundNumber(Math.pow(sliderMinSize, 1.1)), - preferredSize: sliderPreferredSize === (slider.max - 3) ? null : roundNumber(Math.pow(sliderPreferredSize, 1.1)), maxSize: sliderMaxSize === slider.max ? null : roundNumber(Math.pow(sliderMaxSize, 1.1)) }); }; @@ -96,14 +72,12 @@ class QualityDefinition extends Component { onAfterSliderChange = () => { const { minSize, - maxSize, - preferredSize + maxSize } = this.props; this.setState({ sliderMiSize: getSliderValue(minSize, slider.min), - sliderMaxSize: getSliderValue(maxSize, slider.max), - sliderPreferredSize: getSliderValue(preferredSize, (slider.max - 3)) // fix + sliderMaxSize: getSliderValue(maxSize, slider.max) }); }; @@ -116,22 +90,7 @@ class QualityDefinition extends Component { this.props.onSizeChange({ minSize, - maxSize: this.props.maxSize, - preferredSize: this.props.preferredSize - }); - }; - - onPreferredSizeChange = ({ value }) => { - const preferredSize = value === (MAX - 3) ? null : getValue(value); - - this.setState({ - sliderPreferredSize: getSliderValue(preferredSize, slider.preferred) - }); - - this.props.onSizeChange({ - minSize: this.props.minSize, - maxSize: this.props.maxSize, - preferredSize + maxSize: this.props.maxSize }); }; @@ -144,8 +103,7 @@ class QualityDefinition extends Component { this.props.onSizeChange({ minSize: this.props.minSize, - maxSize, - preferredSize: this.props.preferredSize + maxSize }); }; @@ -159,25 +117,20 @@ class QualityDefinition extends Component { title, minSize, maxSize, - preferredSize, advancedSettings, onTitleChange } = this.props; const { sliderMinSize, - sliderMaxSize, - sliderPreferredSize + sliderMaxSize } = this.state; const minBytes = minSize * 128; - const minRate = `${formatBytes(minBytes, true)}/s`; - - const preferredBytes = preferredSize * 128; - const preferredRate = preferredBytes ? `${formatBytes(preferredBytes, true)}/s` : translate('Unlimited'); - const maxBytes = maxSize && maxSize * 128; - const maxRate = maxBytes ? `${formatBytes(maxBytes, true)}/s` : translate('Unlimited'); + + const minRate = `${formatBytes(minBytes, true)}/s`; + const maxRate = maxBytes ? `${formatBytes(maxBytes, true)}/s` : 'Unlimited'; return (
@@ -195,18 +148,16 @@ class QualityDefinition extends Component {
@@ -221,23 +172,7 @@ class QualityDefinition extends Component { body={ - } - position={tooltipPositions.BOTTOM} - /> -
- -
- {preferredRate} - } - title={translate('PreferredSize')} - body={ - } position={tooltipPositions.BOTTOM} @@ -253,7 +188,7 @@ class QualityDefinition extends Component { body={ } position={tooltipPositions.BOTTOM} @@ -266,14 +201,14 @@ class QualityDefinition extends Component { advancedSettings &&
- {translate('Min')} + Min
- {translate('Preferred')} - - -
- -
- {translate('Max')} + Max { + onSizeChange = ({ minSize, maxSize }) => { const { id, minSize: currentMinSize, - maxSize: currentMaxSize, - preferredSize: currentPreferredSize + maxSize: currentMaxSize } = this.props; if (minSize !== currentMinSize) { @@ -38,10 +37,6 @@ class QualityDefinitionConnector extends Component { if (maxSize !== currentMaxSize) { this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize }); } - - if (preferredSize !== currentPreferredSize) { - this.props.setQualityDefinitionValue({ id, name: 'preferredSize', value: preferredSize }); - } }; // @@ -62,7 +57,6 @@ QualityDefinitionConnector.propTypes = { id: PropTypes.number.isRequired, minSize: PropTypes.number, maxSize: PropTypes.number, - preferredSize: PropTypes.number, setQualityDefinitionValue: PropTypes.func.isRequired, clearPendingChanges: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Settings.js b/frontend/src/Settings/Settings.js index d2a86adc6..36f47d82e 100644 --- a/frontend/src/Settings/Settings.js +++ b/frontend/src/Settings/Settings.js @@ -18,132 +18,132 @@ function Settings() { className={styles.link} to="/settings/mediamanagement" > - {translate('MediaManagement')} + Media Management
- {translate('MediaManagementSettingsSummary')} + Naming, file management settings and root folders
- {translate('Profiles')} + Profiles
- {translate('ProfilesSettingsArtistSummary')} + Quality, Metadata, Delay, and Release profiles
- {translate('Quality')} + Quality
- {translate('QualitySettingsSummary')} + Quality sizes and naming
- {translate('CustomFormats')} + Custom Formats
- {translate('CustomFormatsSettingsSummary')} + Custom Formats and Settings
- {translate('Indexers')} + Indexers
- {translate('IndexersSettingsSummary')} + Indexers and indexer options
- {translate('DownloadClients')} + Download Clients
- {translate('DownloadClientsSettingsSummary')} + Download clients, download handling and remote path mappings
- {translate('ImportLists')} + Import Lists
- {translate('ImportListsSettingsSummary')} + Import Lists
- {translate('Connect')} + Connect
- {translate('ConnectSettingsSummary')} + Notifications, connections to media servers/players and custom scripts
- {translate('Metadata')} + Metadata
- {translate('MetadataSettingsArtistSummary')} + Create metadata files when tracks are imported or artist are refreshed
- {translate('Tags')} + Tags
- {translate('TagsSettingsSummary')} + Manage artist, profile, restriction, and notification tags
- {translate('General')} + General
- {translate('GeneralSettingsSummary')} + Port, SSL, username/password, proxy, analytics and updates
- {translate('Ui')} + UI
- {translate('UiSettingsSummary')} + Calendar, date and color impaired options
diff --git a/frontend/src/Settings/SettingsToolbarConnector.js b/frontend/src/Settings/SettingsToolbarConnector.js index 65d937ab8..1e6f7a589 100644 --- a/frontend/src/Settings/SettingsToolbarConnector.js +++ b/frontend/src/Settings/SettingsToolbarConnector.js @@ -134,7 +134,6 @@ const historyShape = { }; SettingsToolbarConnector.propTypes = { - showSave: PropTypes.bool, hasPendingChanges: PropTypes.bool.isRequired, history: PropTypes.shape(historyShape).isRequired, onSavePress: PropTypes.func, diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.css b/frontend/src/Settings/Tags/AutoTagging/AutoTagging.css deleted file mode 100644 index b1e2de95b..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.css +++ /dev/null @@ -1,38 +0,0 @@ -.autoTagging { - composes: card from '~Components/Card.css'; - - width: 300px; -} - -.nameContainer { - display: flex; - justify-content: space-between; -} - -.name { - @add-mixin truncate; - - margin-bottom: 20px; - font-weight: 300; - font-size: 24px; -} - -.cloneButton { - composes: button from '~Components/Link/IconButton.css'; - - height: 36px; -} - -.formats { - display: flex; - flex-wrap: wrap; - margin-top: 5px; - pointer-events: all; -} - -.tooltipLabel { - composes: label from '~Components/Label.css'; - - margin: 0; - border: none; -} diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/AutoTagging.css.d.ts deleted file mode 100644 index b6b665429..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'autoTagging': string; - 'cloneButton': string; - 'formats': string; - 'name': string; - 'nameContainer': string; - 'tooltipLabel': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js b/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js deleted file mode 100644 index 760273cb3..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js +++ /dev/null @@ -1,136 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useState } from 'react'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TagList from 'Components/TagList'; -import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditAutoTaggingModal from './EditAutoTaggingModal'; -import styles from './AutoTagging.css'; - -export default function AutoTagging(props) { - const { - id, - name, - tags, - tagList, - specifications, - isDeleting, - onConfirmDeleteAutoTagging, - onCloneAutoTaggingPress - } = props; - - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onDeletePress = useCallback(() => { - setIsEditModalOpen(false); - setIsDeleteModalOpen(true); - }, [setIsEditModalOpen, setIsDeleteModalOpen]); - - const onDeleteModalClose = useCallback(() => { - setIsDeleteModalOpen(false); - }, [setIsDeleteModalOpen]); - - const onConfirmDelete = useCallback(() => { - onConfirmDeleteAutoTagging(id); - }, [id, onConfirmDeleteAutoTagging]); - - const onClonePress = useCallback(() => { - onCloneAutoTaggingPress(id); - }, [id, onCloneAutoTaggingPress]); - - return ( - -
-
- {name} -
- -
- -
-
- - - -
- { - specifications.map((item, index) => { - if (!item) { - return null; - } - - let kind = kinds.DEFAULT; - if (item.required) { - kind = kinds.SUCCESS; - } - if (item.negate) { - kind = kinds.DANGER; - } - - return ( - - ); - }) - } -
- - - - -
- ); -} - -AutoTagging.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - specifications: PropTypes.arrayOf(PropTypes.object).isRequired, - tags: PropTypes.arrayOf(PropTypes.number).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - isDeleting: PropTypes.bool.isRequired, - onConfirmDeleteAutoTagging: PropTypes.func.isRequired, - onCloneAutoTaggingPress: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css deleted file mode 100644 index 40950bd5f..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css +++ /dev/null @@ -1,21 +0,0 @@ -.autoTaggings { - display: flex; - flex-wrap: wrap; -} - -.addAutoTagging { - composes: autoTagging from '~./AutoTagging.css'; - - background-color: var(--cardAlternateBackgroundColor); - color: var(--gray); - text-align: center; - font-size: 45px; -} - -.center { - display: inline-block; - padding: 5px 20px 0; - border: 1px solid var(--borderColor); - border-radius: 4px; - background-color: var(--cardCenterBackgroundColor); -} diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css.d.ts deleted file mode 100644 index ef3094d3b..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'addAutoTagging': string; - 'autoTaggings': string; - 'center': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js deleted file mode 100644 index 005547bb7..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Card from 'Components/Card'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import { - cloneAutoTagging, - deleteAutoTagging, - fetchAutoTaggings, - fetchRootFolders -} from 'Store/Actions/settingsActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import AutoTagging from './AutoTagging'; -import EditAutoTaggingModal from './EditAutoTaggingModal'; -import styles from './AutoTaggings.css'; - -export default function AutoTaggings() { - const { - error, - items, - isDeleting, - isFetching, - isPopulated - } = useSelector( - createSortedSectionSelector('settings.autoTaggings', sortByProp('name')) - ); - - const tagList = useSelector(createTagsSelector()); - const dispatch = useDispatch(); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [tagsFromId, setTagsFromId] = useState(undefined); - - const onClonePress = useCallback((id) => { - dispatch(cloneAutoTagging({ id })); - - setTagsFromId(id); - setIsEditModalOpen(true); - }, [dispatch, setIsEditModalOpen]); - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onConfirmDelete = useCallback((id) => { - dispatch(deleteAutoTagging({ id })); - }, [dispatch]); - - useEffect(() => { - dispatch(fetchAutoTaggings()); - dispatch(fetchRootFolders()); - }, [dispatch]); - - return ( -
- -
- { - items.map((item) => { - return ( - - ); - }) - } - - -
- -
-
-
- - - -
-
- ); -} diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.js b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.js deleted file mode 100644 index c6f810785..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditAutoTaggingModalContent from './EditAutoTaggingModalContent'; - -export default function EditAutoTaggingModal(props) { - const { - isOpen, - onModalClose: onOriginalModalClose, - ...otherProps - } = props; - - const dispatch = useDispatch(); - const [height, setHeight] = useState('auto'); - - const onContentHeightChange = useCallback((h) => { - if (height === 'auto' || h > height) { - setHeight(h); - } - }, [height, setHeight]); - - const onModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section: 'settings.autoTaggings' })); - onOriginalModalClose(); - }, [dispatch, onOriginalModalClose]); - - return ( - - - - ); -} - -EditAutoTaggingModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css deleted file mode 100644 index d503b0af3..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css +++ /dev/null @@ -1,32 +0,0 @@ -.deleteButton { - composes: button from '~Components/Link/Button.css'; - - margin-right: auto; -} - -.rightButtons { - justify-content: flex-end; - margin-right: auto; -} - -.addSpecification { - composes: autoTagging from '~./AutoTagging.css'; - - background-color: var(--cardAlternateBackgroundColor); - color: var(--gray); - text-align: center; - font-size: 45px; -} - -.center { - display: inline-block; - padding: 5px 20px 0; - border: 1px solid var(--borderColor); - border-radius: 4px; - background-color: var(--cardCenterBackgroundColor); -} - -.autoTaggings { - display: flex; - flex-wrap: wrap; -} diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts deleted file mode 100644 index 2a7f6b41e..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'addSpecification': string; - 'autoTaggings': string; - 'center': string; - 'deleteButton': string; - 'rightButtons': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js deleted file mode 100644 index 01a5e846b..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js +++ /dev/null @@ -1,269 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Card from 'Components/Card'; -import FieldSet from 'Components/FieldSet'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { icons, inputTypes, kinds } from 'Helpers/Props'; -import { - cloneAutoTaggingSpecification, - deleteAutoTaggingSpecification, - fetchAutoTaggingSpecifications, - saveAutoTagging, - setAutoTaggingValue -} from 'Store/Actions/settingsActions'; -import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector'; -import translate from 'Utilities/String/translate'; -import AddSpecificationModal from './Specifications/AddSpecificationModal'; -import EditSpecificationModal from './Specifications/EditSpecificationModal'; -import Specification from './Specifications/Specification'; -import styles from './EditAutoTaggingModalContent.css'; - -export default function EditAutoTaggingModalContent(props) { - const { - id, - tagsFromId, - onModalClose, - onDeleteAutoTaggingPress - } = props; - - const { - error, - item, - isFetching, - isSaving, - saveError, - validationErrors, - validationWarnings - } = useSelector(createProviderSettingsSelectorHook('autoTaggings', id)); - - const { - isPopulated: specificationsPopulated, - items: specifications - } = useSelector((state) => state.settings.autoTaggingSpecifications); - - const dispatch = useDispatch(); - const [isAddSpecificationModalOpen, setIsAddSpecificationModalOpen] = useState(false); - const [isEditSpecificationModalOpen, setIsEditSpecificationModalOpen] = useState(false); - // const [isImportAutoTaggingModalOpen, setIsImportAutoTaggingModalOpen] = useState(false); - - const onAddSpecificationPress = useCallback(() => { - setIsAddSpecificationModalOpen(true); - }, [setIsAddSpecificationModalOpen]); - - const onAddSpecificationModalClose = useCallback(({ specificationSelected = false } = {}) => { - setIsAddSpecificationModalOpen(false); - setIsEditSpecificationModalOpen(specificationSelected); - }, [setIsAddSpecificationModalOpen]); - - const onEditSpecificationModalClose = useCallback(() => { - setIsEditSpecificationModalOpen(false); - }, [setIsEditSpecificationModalOpen]); - - const onInputChange = useCallback(({ name, value }) => { - dispatch(setAutoTaggingValue({ name, value })); - }, [dispatch]); - - const onSavePress = useCallback(() => { - dispatch(saveAutoTagging({ id })); - }, [dispatch, id]); - - const onCloneSpecificationPress = useCallback((specId) => { - dispatch(cloneAutoTaggingSpecification({ id: specId })); - }, [dispatch]); - - const onConfirmDeleteSpecification = useCallback((specId) => { - dispatch(deleteAutoTaggingSpecification({ id: specId })); - }, [dispatch]); - - useEffect(() => { - dispatch(fetchAutoTaggingSpecifications({ id: tagsFromId || id })); - }, [id, tagsFromId, dispatch]); - - const isSavingRef = useRef(); - - useEffect(() => { - if (isSavingRef.current && !isSaving && !saveError) { - onModalClose(); - } - - isSavingRef.current = isSaving; - }, [isSaving, saveError, onModalClose]); - - const { - name, - removeTagsAutomatically, - tags - } = item; - - return ( - - - - {id ? translate('EditAutoTag') : translate('AddAutoTag')} - - - -
- { - isFetching ? : null - } - - { - !isFetching && !!error ? -
- {translate('AddAutoTagError')} -
: - null - } - - { - !isFetching && !error && specificationsPopulated ? -
-
- - - {translate('Name')} - - - - - - - {translate('RemoveTagsAutomatically')} - - - - - - {translate('Tags')} - - - - - -
-
- { - specifications.map((tag) => { - return ( - - ); - }) - } - - -
- -
-
-
-
- - - - - - {/* */} - -
: - null - } -
-
- -
- { - id ? - : - null - } - - {/* */} -
- - - - - {translate('Save')} - -
-
- ); -} - -EditAutoTaggingModalContent.propTypes = { - id: PropTypes.number, - tagsFromId: PropTypes.number, - onModalClose: PropTypes.func.isRequired, - onDeleteAutoTaggingPress: PropTypes.func -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css deleted file mode 100644 index eabcae750..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css +++ /dev/null @@ -1,44 +0,0 @@ -.specification { - composes: card from '~Components/Card.css'; - - position: relative; - width: 300px; - height: 100px; -} - -.underlay { - @add-mixin cover; -} - -.overlay { - @add-mixin linkOverlay; - - padding: 10px; -} - -.name { - text-align: center; - font-weight: lighter; - font-size: 24px; -} - -.actions { - margin-top: 20px; - text-align: right; -} - -.presetsMenu { - composes: menu from '~Components/Menu/Menu.css'; - - display: inline-block; - margin: 0 5px; -} - -.presetsMenuButton { - composes: button from '~Components/Link/Button.css'; - - &::after { - margin-left: 5px; - content: '\25BE'; - } -} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css.d.ts deleted file mode 100644 index 7f8a93de9..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'actions': string; - 'name': string; - 'overlay': string; - 'presetsMenu': string; - 'presetsMenuButton': string; - 'specification': string; - 'underlay': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.js deleted file mode 100644 index f6f2b134e..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback } from 'react'; -import Button from 'Components/Link/Button'; -import Link from 'Components/Link/Link'; -import Menu from 'Components/Menu/Menu'; -import MenuContent from 'Components/Menu/MenuContent'; -import { sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import AddSpecificationPresetMenuItem from './AddSpecificationPresetMenuItem'; -import styles from './AddSpecificationItem.css'; - -export default function AddSpecificationItem(props) { - const { - implementation, - implementationName, - infoLink, - presets, - onSpecificationSelect - } = props; - - const onWrappedSpecificationSelect = useCallback(() => { - onSpecificationSelect({ implementation }); - }, [implementation, onSpecificationSelect]); - - const hasPresets = !!presets && !!presets.length; - - return ( -
- - -
-
- {implementationName} -
- -
- { - hasPresets ? - - - - - - - - { - presets.map((preset, index) => { - return ( - - ); - }) - } - - - : - null - } - - { - infoLink ? - : - null - } -
-
-
- ); -} - -AddSpecificationItem.propTypes = { - implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, - infoLink: PropTypes.string, - presets: PropTypes.arrayOf(PropTypes.object), - onSpecificationSelect: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css deleted file mode 100644 index d51349ea9..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css +++ /dev/null @@ -1,5 +0,0 @@ -.specifications { - display: flex; - justify-content: center; - flex-wrap: wrap; -} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css.d.ts deleted file mode 100644 index 83fbf5804..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'specifications': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js deleted file mode 100644 index 454a2591a..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Alert from 'Components/Alert'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { kinds } from 'Helpers/Props'; -import { - fetchAutoTaggingSpecificationSchema, - selectAutoTaggingSpecificationSchema -} from 'Store/Actions/settingsActions'; -import translate from 'Utilities/String/translate'; -import AddSpecificationItem from './AddSpecificationItem'; -import styles from './AddSpecificationModalContent.css'; - -export default function AddSpecificationModalContent(props) { - const { - onModalClose - } = props; - - const { - isSchemaFetching, - isSchemaPopulated, - schemaError, - schema - } = useSelector( - (state) => state.settings.autoTaggingSpecifications - ); - - const dispatch = useDispatch(); - - const onSpecificationSelect = useCallback(({ implementation, name }) => { - dispatch(selectAutoTaggingSpecificationSchema({ implementation, presetName: name })); - onModalClose({ specificationSelected: true }); - }, [dispatch, onModalClose]); - - useEffect(() => { - dispatch(fetchAutoTaggingSpecificationSchema()); - }, [dispatch]); - - return ( - - - {translate('AddCondition')} - - - - { - isSchemaFetching ? : null - } - - { - !isSchemaFetching && !!schemaError ? -
- {translate('AddConditionError')} -
: - null - } - - { - isSchemaPopulated && !schemaError ? -
- - -
- {translate('SupportedAutoTaggingProperties')} -
-
- -
- { - schema.map((specification) => { - return ( - - ); - }) - } -
- -
: - null - } -
- - - - -
- ); -} - -AddSpecificationModalContent.propTypes = { - onModalClose: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.js deleted file mode 100644 index b043ddf06..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback } from 'react'; -import MenuItem from 'Components/Menu/MenuItem'; - -export default function AddSpecificationPresetMenuItem(props) { - const { - name, - implementation, - onPress, - ...otherProps - } = props; - - const onWrappedPress = useCallback(() => { - onPress({ - name, - implementation - }); - }, [name, implementation, onPress]); - - return ( - - {name} - - ); -} - -AddSpecificationPresetMenuItem.propTypes = { - name: PropTypes.string.isRequired, - implementation: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.js deleted file mode 100644 index 16ed4daec..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.js +++ /dev/null @@ -1,36 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditSpecificationModalContent from './EditSpecificationModalContent'; - -function EditSpecificationModal({ isOpen, onModalClose, ...otherProps }) { - const dispatch = useDispatch(); - - const onWrappedModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section: 'settings.autoTaggingSpecifications' })); - onModalClose(); - }, [onModalClose, dispatch]); - - return ( - - - - ); -} - -EditSpecificationModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditSpecificationModal; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css deleted file mode 100644 index a2b6014df..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css +++ /dev/null @@ -1,5 +0,0 @@ -.deleteButton { - composes: button from '~Components/Link/Button.css'; - - margin-right: auto; -} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css.d.ts deleted file mode 100644 index c5f0ef8a7..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'deleteButton': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js deleted file mode 100644 index 04302729b..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js +++ /dev/null @@ -1,190 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Alert from 'Components/Alert'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds } from 'Helpers/Props'; -import { - clearAutoTaggingSpecificationPending, - saveAutoTaggingSpecification, - setAutoTaggingSpecificationFieldValue, - setAutoTaggingSpecificationValue -} from 'Store/Actions/settingsActions'; -import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector'; -import translate from 'Utilities/String/translate'; -import styles from './EditSpecificationModalContent.css'; - -function EditSpecificationModalContent(props) { - const { - id, - onDeleteSpecificationPress, - onModalClose - } = props; - - const advancedSettings = useSelector((state) => state.settings.advancedSettings); - - const { - item, - ...otherFormProps - } = useSelector( - createProviderSettingsSelectorHook('autoTaggingSpecifications', id) - ); - - const dispatch = useDispatch(); - - const onInputChange = useCallback(({ name, value }) => { - dispatch(setAutoTaggingSpecificationValue({ name, value })); - }, [dispatch]); - - const onFieldChange = useCallback(({ name, value }) => { - dispatch(setAutoTaggingSpecificationFieldValue({ name, value })); - }, [dispatch]); - - const onCancelPress = useCallback(({ name, value }) => { - dispatch(clearAutoTaggingSpecificationPending()); - onModalClose(); - }, [dispatch, onModalClose]); - - const onSavePress = useCallback(({ name, value }) => { - dispatch(saveAutoTaggingSpecification({ id })); - onModalClose(); - }, [dispatch, id, onModalClose]); - - const { - implementationName, - name, - negate, - required, - fields - } = item; - - return ( - - - {id ? translate('EditConditionImplementation', { implementationName }) : translate('AddConditionImplementation', { implementationName })} - - - -
- { - fields && fields.some((x) => x.label === 'Regular Expression') && - -
- -
-
- -
-
- -
-
- } - - - - {translate('Name')} - - - - - - { - fields && fields.map((field) => { - return ( - - ); - }) - } - - - - {translate('Negate')} - - - - - - - - {translate('Required')} - - - - - -
- - { - id ? - : - null - } - - - - - {translate('Save')} - - -
- ); -} - -EditSpecificationModalContent.propTypes = { - id: PropTypes.number, - onDeleteSpecificationPress: PropTypes.func, - onModalClose: PropTypes.func.isRequired -}; - -export default EditSpecificationModalContent; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContentConnector.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContentConnector.js deleted file mode 100644 index 8f27b74e0..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContentConnector.js +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearAutoTaggingSpecificationPending, saveAutoTaggingSpecification, setAutoTaggingSpecificationFieldValue, setAutoTaggingSpecificationValue } from 'Store/Actions/settingsActions'; -import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; -import EditSpecificationModalContent from './EditSpecificationModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - createProviderSettingsSelector('autoTaggingSpecifications'), - (advancedSettings, specification) => { - return { - advancedSettings, - ...specification - }; - } - ); -} - -const mapDispatchToProps = { - setAutoTaggingSpecificationValue, - setAutoTaggingSpecificationFieldValue, - saveAutoTaggingSpecification, - clearAutoTaggingSpecificationPending -}; - -class EditSpecificationModalContentConnector extends Component { - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setAutoTaggingSpecificationValue({ name, value }); - }; - - onFieldChange = ({ name, value }) => { - this.props.setAutoTaggingSpecificationFieldValue({ name, value }); - }; - - onCancelPress = () => { - this.props.clearAutoTaggingSpecificationPending(); - this.props.onModalClose(); - }; - - onSavePress = () => { - this.props.saveAutoTaggingSpecification({ id: this.props.id }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditSpecificationModalContentConnector.propTypes = { - id: PropTypes.number, - item: PropTypes.object.isRequired, - setAutoTaggingSpecificationValue: PropTypes.func.isRequired, - setAutoTaggingSpecificationFieldValue: PropTypes.func.isRequired, - clearAutoTaggingSpecificationPending: PropTypes.func.isRequired, - saveAutoTaggingSpecification: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditSpecificationModalContentConnector); diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css b/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css deleted file mode 100644 index e329fc313..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css +++ /dev/null @@ -1,38 +0,0 @@ -.autoTagging { - composes: card from '~Components/Card.css'; - - width: 300px; -} - -.nameContainer { - display: flex; - justify-content: space-between; -} - -.name { - @add-mixin truncate; - - margin-bottom: 20px; - font-weight: 300; - font-size: 24px; -} - -.cloneButton { - composes: button from '~Components/Link/IconButton.css'; - - height: 36px; -} - -.labels { - display: flex; - flex-wrap: wrap; - margin-top: 5px; - pointer-events: all; -} - -.tooltipLabel { - composes: label from '~Components/Label.css'; - - margin: 0; - border: none; -} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css.d.ts b/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css.d.ts deleted file mode 100644 index b3229d715..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'autoTagging': string; - 'cloneButton': string; - 'labels': string; - 'name': string; - 'nameContainer': string; - 'tooltipLabel': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.js deleted file mode 100644 index 21977e160..000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.js +++ /dev/null @@ -1,122 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useState } from 'react'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditSpecificationModal from './EditSpecificationModal'; -import styles from './Specification.css'; - -export default function Specification(props) { - const { - id, - implementationName, - name, - required, - negate, - onConfirmDeleteSpecification, - onCloneSpecificationPress - } = props; - - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onDeletePress = useCallback(() => { - setIsEditModalOpen(false); - setIsDeleteModalOpen(true); - }, [setIsEditModalOpen, setIsDeleteModalOpen]); - - const onDeleteModalClose = useCallback(() => { - setIsDeleteModalOpen(false); - }, [setIsDeleteModalOpen]); - - const onConfirmDelete = useCallback(() => { - onConfirmDeleteSpecification(id); - }, [id, onConfirmDeleteSpecification]); - - const onClonePress = useCallback(() => { - onCloneSpecificationPress(id); - }, [id, onCloneSpecificationPress]); - - return ( - -
-
- {name} -
- - -
- -
- - - { - negate ? - : - null - } - - { - required ? - : - null - } -
- - - - -
- ); -} - -Specification.propTypes = { - id: PropTypes.number.isRequired, - implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - negate: PropTypes.bool.isRequired, - required: PropTypes.bool.isRequired, - fields: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteSpecification: PropTypes.func.isRequired, - onCloneSpecificationPress: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js index 78372d5a3..4473ddfef 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js @@ -23,7 +23,6 @@ function TagDetailsModalContent(props) { releaseProfiles, indexers, downloadClients, - autoTags, onModalClose, onDeleteTagPress } = props; @@ -198,22 +197,6 @@ function TagDetailsModalContent(props) { : null } - - { - autoTags.length ? -
- { - autoTags.map((item) => { - return ( -
- {item.name} -
- ); - }) - } -
: - null - } @@ -249,7 +232,6 @@ TagDetailsModalContent.propTypes = { releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired, indexers: PropTypes.arrayOf(PropTypes.object).isRequired, downloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, - autoTags: PropTypes.arrayOf(PropTypes.object).isRequired, onModalClose: PropTypes.func.isRequired, onDeleteTagPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js index ddd70b253..d2342d52d 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js @@ -85,14 +85,6 @@ function createMatchingDownloadClientsSelector() { ); } -function createMatchingAutoTagsSelector() { - return createSelector( - (state, { autoTagIds }) => autoTagIds, - (state) => state.settings.autoTaggings.items, - findMatchingItems - ); -} - function createMapStateToProps() { return createSelector( createMatchingArtistSelector(), @@ -102,8 +94,7 @@ function createMapStateToProps() { createMatchingReleaseProfilesSelector(), createMatchingIndexersSelector(), createMatchingDownloadClientsSelector(), - createMatchingAutoTagsSelector(), - (artist, delayProfiles, importLists, notifications, releaseProfiles, indexers, downloadClients, autoTags) => { + (artist, delayProfiles, importLists, notifications, releaseProfiles, indexers, downloadClients) => { return { artist, delayProfiles, @@ -111,8 +102,7 @@ function createMapStateToProps() { notifications, releaseProfiles, indexers, - downloadClients, - autoTags + downloadClients }; } ); diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js index 525bf5844..9a0ff0bff 100644 --- a/frontend/src/Settings/Tags/Tag.js +++ b/frontend/src/Settings/Tags/Tag.js @@ -5,7 +5,6 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import TagDetailsModal from './Details/TagDetailsModal'; -import TagInUse from './TagInUse'; import styles from './Tag.css'; class Tag extends Component { @@ -58,10 +57,9 @@ class Tag extends Component { importListIds, notificationIds, restrictionIds, + artistIds, indexerIds, - downloadClientIds, - autoTagIds, - artistIds + downloadClientIds } = this.props; const { @@ -74,10 +72,9 @@ class Tag extends Component { importListIds.length || notificationIds.length || restrictionIds.length || + artistIds.length || indexerIds.length || - downloadClientIds.length || - autoTagIds.length || - artistIds.length + downloadClientIds.length ); return ( @@ -91,56 +88,63 @@ class Tag extends Component {
{ - isTagUsed ? + isTagUsed &&
- + { + artistIds.length ? +
+ {artistIds.length} artists +
: + null + } - + { + delayProfileIds.length ? +
+ {delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'} +
: + null + } - + { + importListIds.length ? +
+ {importListIds.length} import list{importListIds.length > 1 && 's'} +
: + null + } - + { + notificationIds.length ? +
+ {notificationIds.length} connection{notificationIds.length > 1 && 's'} +
: + null + } - + { + restrictionIds.length ? +
+ {restrictionIds.length} restriction{restrictionIds.length > 1 && 's'} +
: + null + } + { + indexerIds.length ? +
+ {indexerIds.length} indexer{indexerIds.length > 1 && 's'} +
: + null + } - - - - - -
: - null + { + downloadClientIds.length ? +
+ {downloadClientIds.length} download client{indexerIds.length > 1 && 's'} +
: + null + } +
} { @@ -160,7 +164,6 @@ class Tag extends Component { restrictionIds={restrictionIds} indexerIds={indexerIds} downloadClientIds={downloadClientIds} - autoTagIds={autoTagIds} isOpen={isDetailsModalOpen} onModalClose={this.onDetailsModalClose} onDeleteTagPress={this.onDeleteTagPress} @@ -170,7 +173,7 @@ class Tag extends Component { isOpen={isDeleteTagModalOpen} kind={kinds.DANGER} title={translate('DeleteTag')} - message={translate('DeleteTagMessageText', { label })} + message={translate('DeleteTagMessageText', [label])} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteTag} onCancel={this.onDeleteTagModalClose} @@ -187,10 +190,9 @@ Tag.propTypes = { importListIds: PropTypes.arrayOf(PropTypes.number).isRequired, notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired, restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired, + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired, downloadClientIds: PropTypes.arrayOf(PropTypes.number).isRequired, - autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired, - artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, onConfirmDeleteTag: PropTypes.func.isRequired }; @@ -199,10 +201,9 @@ Tag.defaultProps = { importListIds: [], notificationIds: [], restrictionIds: [], + artistIds: [], indexerIds: [], - downloadClientIds: [], - autoTagIds: [], - artistIds: [] + downloadClientIds: [] }; export default Tag; diff --git a/frontend/src/Settings/Tags/TagInUse.js b/frontend/src/Settings/Tags/TagInUse.js deleted file mode 100644 index 27228fa2e..000000000 --- a/frontend/src/Settings/Tags/TagInUse.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -export default function TagInUse(props) { - const { - label, - labelPlural, - count - } = props; - - if (count === 0) { - return null; - } - - if (count > 1 && labelPlural) { - return ( -
- {count} {labelPlural.toLowerCase()} -
- ); - } - - return ( -
- {count} {label.toLowerCase()} -
- ); -} - -TagInUse.propTypes = { - label: PropTypes.string.isRequired, - labelPlural: PropTypes.string, - count: PropTypes.number.isRequired -}; diff --git a/frontend/src/Settings/Tags/TagSettings.js b/frontend/src/Settings/Tags/TagSettings.js index ca8672603..ad2e499eb 100644 --- a/frontend/src/Settings/Tags/TagSettings.js +++ b/frontend/src/Settings/Tags/TagSettings.js @@ -3,7 +3,6 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import AutoTaggings from './AutoTagging/AutoTaggings'; import TagsConnector from './TagsConnector'; function TagSettings() { @@ -15,7 +14,6 @@ function TagSettings() { - ); diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 15f31d3c5..770dc4720 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -3,14 +3,12 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; -import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import { fetchTagDetails } from 'Store/Actions/tagActions'; import Tags from './Tags'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('tags', sortByProp('label')), + (state) => state.tags, (tags) => { const isFetching = tags.isFetching || tags.details.isFetching; const error = tags.error || tags.details.error; @@ -27,7 +25,6 @@ function createMapStateToProps() { } const mapDispatchToProps = { - dispatchFetchTags: fetchTags, dispatchFetchTagDetails: fetchTagDetails, dispatchFetchDelayProfiles: fetchDelayProfiles, dispatchFetchImportLists: fetchImportLists, @@ -44,7 +41,6 @@ class MetadatasConnector extends Component { componentDidMount() { const { - dispatchFetchTags, dispatchFetchTagDetails, dispatchFetchDelayProfiles, dispatchFetchImportLists, @@ -54,7 +50,6 @@ class MetadatasConnector extends Component { dispatchFetchDownloadClients } = this.props; - dispatchFetchTags(); dispatchFetchTagDetails(); dispatchFetchDelayProfiles(); dispatchFetchImportLists(); @@ -77,7 +72,6 @@ class MetadatasConnector extends Component { } MetadatasConnector.propTypes = { - dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchDelayProfiles: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index cc27829df..f6219406f 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -22,19 +22,19 @@ export const firstDayOfWeekOptions = [ ]; export const weekColumnOptions = [ - { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' }, - { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' }, - { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' }, - { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' } + { key: 'ddd M/D', value: 'Tue 3/25' }, + { key: 'ddd MM/DD', value: 'Tue 03/25' }, + { key: 'ddd D/M', value: 'Tue 25/3' }, + { key: 'ddd DD/MM', value: 'Tue 25/03' } ]; const shortDateFormatOptions = [ - { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' }, - { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' }, - { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' }, - { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' }, - { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' }, - { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' } + { key: 'MMM D YYYY', value: 'Mar 25 2014' }, + { key: 'DD MMM YYYY', value: '25 Mar 2014' }, + { key: 'MM/D/YYYY', value: '03/25/2014' }, + { key: 'MM/DD/YYYY', value: '03/25/2014' }, + { key: 'DD/MM/YYYY', value: '25/03/2014' }, + { key: 'YYYY-MM-DD', value: '2014-03-25' } ]; const longDateFormatOptions = [ @@ -69,7 +69,7 @@ class UISettings extends Component { .map((theme) => ({ key: theme, value: titleCase(theme) })); return ( - +
- {translate('UiLanguage')} + {translate('UILanguage')} language.key === settings.uiLanguage.value) ? - settings.uiLanguage.errors : - [ - ...settings.uiLanguage.errors, - { message: translate('InvalidUILanguage') } - ]} />
diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js index f5ef10a4d..a80ee1e45 100644 --- a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -6,8 +6,6 @@ import getSectionState from 'Utilities/State/getSectionState'; import { set, updateServerSideCollection } from '../baseActions'; function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) { - const [baseSection] = section.split('.'); - return function(getState, payload, dispatch) { dispatch(set({ section, isFetching: true })); @@ -27,13 +25,10 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const { selectedFilterKey, - filters + filters, + customFilters } = sectionState; - const customFilters = getState().customFilters.items.filter((customFilter) => { - return customFilter.type === section || customFilter.type === baseSection; - }); - const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters); selectedFilters.forEach((filter) => { @@ -42,8 +37,7 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const promise = createAjaxRequest({ url, - data, - traditional: true + data }).request; promise.done((response) => { diff --git a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js index 3de794bdf..dfe29ace8 100644 --- a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js +++ b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js @@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) { return function(getState, payload, dispatch) { const { id, - queryParams + ...queryParams } = payload; dispatch(set({ section, isDeleting: true })); diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index e35157dbd..ca26883fb 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -1,11 +1,8 @@ -import $ from 'jquery'; -import _ from 'lodash'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getProviderState from 'Utilities/State/getProviderState'; import { set } from '../baseActions'; const abortCurrentRequests = {}; -let lastTestData = null; export function createCancelTestProviderHandler(section) { return function(getState, payload, dispatch) { @@ -20,25 +17,10 @@ function createTestProviderHandler(section, url) { return function(getState, payload, dispatch) { dispatch(set({ section, isTesting: true })); - const { - queryParams = {}, - ...otherPayload - } = payload; - - const testData = getProviderState({ ...otherPayload }, getState, section); - const params = { ...queryParams }; - - // If the user is re-testing the same provider without changes - // force it to be tested. - - if (_.isEqual(testData, lastTestData)) { - params.forceTest = true; - } - - lastTestData = testData; + const testData = getProviderState(payload, getState, section); const ajaxOptions = { - url: `${url}/test?${$.param(params, true)}`, + url: `${url}/test`, method: 'POST', contentType: 'application/json', dataType: 'json', @@ -50,8 +32,6 @@ function createTestProviderHandler(section, url) { abortCurrentRequests[section] = abortRequest; request.done((data) => { - lastTestData = null; - dispatch(set({ section, isTesting: false, diff --git a/frontend/src/Store/Actions/Settings/autoTaggingSpecifications.js b/frontend/src/Store/Actions/Settings/autoTaggingSpecifications.js deleted file mode 100644 index cfc919c7d..000000000 --- a/frontend/src/Store/Actions/Settings/autoTaggingSpecifications.js +++ /dev/null @@ -1,193 +0,0 @@ -import { createAction } from 'redux-actions'; -import { batchActions } from 'redux-batched-actions'; -import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; -import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer'; -import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; -import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; -import { createThunk } from 'Store/thunks'; -import getNextId from 'Utilities/State/getNextId'; -import getProviderState from 'Utilities/State/getProviderState'; -import getSectionState from 'Utilities/State/getSectionState'; -import selectProviderSchema from 'Utilities/State/selectProviderSchema'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import { removeItem, set, update, updateItem } from '../baseActions'; - -// -// Variables - -const section = 'settings.autoTaggingSpecifications'; - -// -// Actions Types - -export const FETCH_AUTO_TAGGING_SPECIFICATIONS = 'settings/autoTaggingSpecifications/fetchAutoTaggingSpecifications'; -export const FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA = 'settings/autoTaggingSpecifications/fetchAutoTaggingSpecificationSchema'; -export const SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA = 'settings/autoTaggingSpecifications/selectAutoTaggingSpecificationSchema'; -export const SET_AUTO_TAGGING_SPECIFICATION_VALUE = 'settings/autoTaggingSpecifications/setAutoTaggingSpecificationValue'; -export const SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE = 'settings/autoTaggingSpecifications/setAutoTaggingSpecificationFieldValue'; -export const SAVE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/saveAutoTaggingSpecification'; -export const DELETE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/deleteAutoTaggingSpecification'; -export const DELETE_ALL_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/deleteAllAutoTaggingSpecification'; -export const CLONE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/cloneAutoTaggingSpecification'; -export const CLEAR_AUTO_TAGGING_SPECIFICATIONS = 'settings/autoTaggingSpecifications/clearAutoTaggingSpecifications'; -export const CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING = 'settings/autoTaggingSpecifications/clearAutoTaggingSpecificationPending'; -// -// Action Creators - -export const fetchAutoTaggingSpecifications = createThunk(FETCH_AUTO_TAGGING_SPECIFICATIONS); -export const fetchAutoTaggingSpecificationSchema = createThunk(FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA); -export const selectAutoTaggingSpecificationSchema = createAction(SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA); - -export const saveAutoTaggingSpecification = createThunk(SAVE_AUTO_TAGGING_SPECIFICATION); -export const deleteAutoTaggingSpecification = createThunk(DELETE_AUTO_TAGGING_SPECIFICATION); -export const deleteAllAutoTaggingSpecification = createThunk(DELETE_ALL_AUTO_TAGGING_SPECIFICATION); - -export const setAutoTaggingSpecificationValue = createAction(SET_AUTO_TAGGING_SPECIFICATION_VALUE, (payload) => { - return { - section, - ...payload - }; -}); - -export const setAutoTaggingSpecificationFieldValue = createAction(SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE, (payload) => { - return { - section, - ...payload - }; -}); - -export const cloneAutoTaggingSpecification = createAction(CLONE_AUTO_TAGGING_SPECIFICATION); - -export const clearAutoTaggingSpecification = createAction(CLEAR_AUTO_TAGGING_SPECIFICATIONS); - -export const clearAutoTaggingSpecificationPending = createThunk(CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING); - -// -// Details - -export default { - - // - // State - - defaultState: { - isPopulated: false, - error: null, - isSchemaFetching: false, - isSchemaPopulated: false, - schemaError: null, - schema: [], - selectedSchema: {}, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} - }, - - // - // Action Handlers - - actionHandlers: { - [FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA]: createFetchSchemaHandler(section, '/autoTagging/schema'), - - [FETCH_AUTO_TAGGING_SPECIFICATIONS]: (getState, payload, dispatch) => { - let tags = []; - if (payload.id) { - const cfState = getSectionState(getState(), 'settings.autoTaggings', true); - const cf = cfState.items[cfState.itemMap[payload.id]]; - tags = cf.specifications.map((tag, i) => { - return { - id: i + 1, - ...tag - }; - }); - } - - dispatch(batchActions([ - update({ section, data: tags }), - set({ - section, - isPopulated: true - }) - ])); - }, - - [SAVE_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => { - const { - id, - ...otherPayload - } = payload; - - const saveData = getProviderState({ id, ...otherPayload }, getState, section, false); - - // we have to set id since not actually posting to server yet - if (!saveData.id) { - saveData.id = getNextId(getState().settings.autoTaggingSpecifications.items); - } - - dispatch(batchActions([ - updateItem({ section, ...saveData }), - set({ - section, - pendingChanges: {} - }) - ])); - }, - - [DELETE_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => { - const id = payload.id; - return dispatch(removeItem({ section, id })); - }, - - [DELETE_ALL_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => { - return dispatch(set({ - section, - items: [] - })); - }, - - [CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING]: (getState, payload, dispatch) => { - return dispatch(set({ - section, - pendingChanges: {} - })); - } - }, - - // - // Reducers - - reducers: { - [SET_AUTO_TAGGING_SPECIFICATION_VALUE]: createSetSettingValueReducer(section), - [SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section), - - [SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA]: (state, { payload }) => { - return selectProviderSchema(state, section, payload, (selectedSchema) => { - return selectedSchema; - }); - }, - - [CLONE_AUTO_TAGGING_SPECIFICATION]: function(state, { payload }) { - const id = payload.id; - const newState = getSectionState(state, section); - const items = newState.items; - const item = items.find((i) => i.id === id); - const newId = getNextId(newState.items); - const newItem = { - ...item, - id: newId, - name: `${item.name} - Copy` - }; - newState.items = [...items, newItem]; - newState.itemMap[newId] = newState.items.length - 1; - - return updateSectionState(state, section, newState); - }, - - [CLEAR_AUTO_TAGGING_SPECIFICATIONS]: createClearReducer(section, { - isPopulated: false, - error: null, - items: [] - }) - } -}; diff --git a/frontend/src/Store/Actions/Settings/autoTaggings.js b/frontend/src/Store/Actions/Settings/autoTaggings.js deleted file mode 100644 index 35b3d4149..000000000 --- a/frontend/src/Store/Actions/Settings/autoTaggings.js +++ /dev/null @@ -1,109 +0,0 @@ -import { createAction } from 'redux-actions'; -import { set } from 'Store/Actions/baseActions'; -import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; -import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; -import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; -import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; -import { createThunk } from 'Store/thunks'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; - -// -// Variables - -const section = 'settings.autoTaggings'; - -// -// Actions Types - -export const FETCH_AUTO_TAGGINGS = 'settings/autoTaggings/fetchAutoTaggings'; -export const SAVE_AUTO_TAGGING = 'settings/autoTaggings/saveAutoTagging'; -export const DELETE_AUTO_TAGGING = 'settings/autoTaggings/deleteAutoTagging'; -export const SET_AUTO_TAGGING_VALUE = 'settings/autoTaggings/setAutoTaggingValue'; -export const CLONE_AUTO_TAGGING = 'settings/autoTaggings/cloneAutoTagging'; - -// -// Action Creators - -export const fetchAutoTaggings = createThunk(FETCH_AUTO_TAGGINGS); -export const saveAutoTagging = createThunk(SAVE_AUTO_TAGGING); -export const deleteAutoTagging = createThunk(DELETE_AUTO_TAGGING); - -export const setAutoTaggingValue = createAction(SET_AUTO_TAGGING_VALUE, (payload) => { - return { - section, - ...payload - }; -}); - -export const cloneAutoTagging = createAction(CLONE_AUTO_TAGGING); - -// -// Details - -export default { - - // - // State - - defaultState: { - isSchemaFetching: false, - isSchemaPopulated: false, - isFetching: false, - isPopulated: false, - schema: { - removeTagsAutomatically: false, - tags: [] - }, - error: null, - isDeleting: false, - deleteError: null, - isSaving: false, - saveError: null, - items: [], - pendingChanges: {} - }, - - // - // Action Handlers - - actionHandlers: { - [FETCH_AUTO_TAGGINGS]: createFetchHandler(section, '/autoTagging'), - - [DELETE_AUTO_TAGGING]: createRemoveItemHandler(section, '/autoTagging'), - - [SAVE_AUTO_TAGGING]: (getState, payload, dispatch) => { - // move the format tags in as a pending change - const state = getState(); - const pendingChanges = state.settings.autoTaggings.pendingChanges; - pendingChanges.specifications = state.settings.autoTaggingSpecifications.items; - dispatch(set({ - section, - pendingChanges - })); - - createSaveProviderHandler(section, '/autoTagging')(getState, payload, dispatch); - } - }, - - // - // Reducers - - reducers: { - [SET_AUTO_TAGGING_VALUE]: createSetSettingValueReducer(section), - - [CLONE_AUTO_TAGGING]: function(state, { payload }) { - const id = payload.id; - const newState = getSectionState(state, section); - const item = newState.items.find((i) => i.id === id); - const pendingChanges = { ...item, id: 0 }; - delete pendingChanges.id; - - pendingChanges.name = `${pendingChanges.name} - Copy`; - newState.pendingChanges = pendingChanges; - - return updateSectionState(state, section, newState); - } - } - -}; diff --git a/frontend/src/Store/Actions/Settings/customFormats.js b/frontend/src/Store/Actions/Settings/customFormats.js index 3b8a209f9..4a175abea 100644 --- a/frontend/src/Store/Actions/Settings/customFormats.js +++ b/frontend/src/Store/Actions/Settings/customFormats.js @@ -1,12 +1,7 @@ import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; -import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; -import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; -import createSetClientSideCollectionSortReducer - from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; import getSectionState from 'Utilities/State/getSectionState'; @@ -26,9 +21,6 @@ export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat'; export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat'; export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue'; export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat'; -export const BULK_EDIT_CUSTOM_FORMATS = 'settings/downloadClients/bulkEditCustomFormats'; -export const BULK_DELETE_CUSTOM_FORMATS = 'settings/downloadClients/bulkDeleteCustomFormats'; -export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManageCustomFormatsSort'; // // Action Creators @@ -36,9 +28,6 @@ export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManag export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS); export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT); export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT); -export const bulkEditCustomFormats = createThunk(BULK_EDIT_CUSTOM_FORMATS); -export const bulkDeleteCustomFormats = createThunk(BULK_DELETE_CUSTOM_FORMATS); -export const setManageCustomFormatsSort = createAction(SET_MANAGE_CUSTOM_FORMATS_SORT); export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => { return { @@ -58,30 +47,20 @@ export default { // State defaultState: { - isFetching: false, - isPopulated: false, - error: null, - isSaving: false, - saveError: null, - isDeleting: false, - deleteError: null, - items: [], - pendingChanges: {}, - isSchemaFetching: false, isSchemaPopulated: false, - schemaError: null, + isFetching: false, + isPopulated: false, schema: { includeCustomFormatWhenRenaming: false }, - - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); - } - } + error: null, + isDeleting: false, + deleteError: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} }, // @@ -103,10 +82,7 @@ export default { })); createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch); - }, - - [BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'), - [BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk') + } }, // @@ -126,9 +102,7 @@ export default { newState.pendingChanges = pendingChanges; return updateSectionState(state, section, newState); - }, - - [SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section) + } } }; diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index 1113e7daf..12651039d 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -1,5 +1,4 @@ import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; @@ -8,7 +7,6 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; -import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; @@ -35,7 +33,6 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients'; export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients'; export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients'; -export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort'; // // Action Creators @@ -52,7 +49,6 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT) export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS); export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS); export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS); -export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT); export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => { return { @@ -92,14 +88,7 @@ export default { isTesting: false, isTestingAll: false, items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); - } - } + pendingChanges: {} }, // @@ -133,10 +122,7 @@ export default { return selectedSchema; }); - }, - - [SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section) - + } } }; diff --git a/frontend/src/Store/Actions/Settings/indexerFlags.js b/frontend/src/Store/Actions/Settings/indexerFlags.js deleted file mode 100644 index a53fe1c61..000000000 --- a/frontend/src/Store/Actions/Settings/indexerFlags.js +++ /dev/null @@ -1,48 +0,0 @@ -import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; -import { createThunk } from 'Store/thunks'; - -// -// Variables - -const section = 'settings.indexerFlags'; - -// -// Actions Types - -export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags'; - -// -// Action Creators - -export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS); - -// -// Details - -export default { - - // - // State - - defaultState: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - // - // Action Handlers - - actionHandlers: { - [FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag') - }, - - // - // Reducers - - reducers: { - - } - -}; diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js index 511a2e475..c76750cf2 100644 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ b/frontend/src/Store/Actions/Settings/indexers.js @@ -1,5 +1,4 @@ import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; @@ -8,7 +7,6 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; -import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; @@ -38,7 +36,6 @@ export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer'; export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers'; export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers'; export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers'; -export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort'; // // Action Creators @@ -56,7 +53,6 @@ export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER); export const testAllIndexers = createThunk(TEST_ALL_INDEXERS); export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS); export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS); -export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT); export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => { return { @@ -96,14 +92,7 @@ export default { isTesting: false, isTestingAll: false, items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); - } - } + pendingChanges: {} }, // @@ -153,13 +142,7 @@ export default { delete selectedSchema.name; selectedSchema.fields = selectedSchema.fields.map((field) => { - const newField = { ...field }; - - if (newField.privacy === 'apiKey' || newField.privacy === 'password') { - newField.value = ''; - } - - return newField; + return { ...field }; }); newState.selectedSchema = selectedSchema; @@ -170,10 +153,7 @@ export default { }; return updateSectionState(state, section, newState); - }, - - [SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section) - + } } }; diff --git a/frontend/src/Store/Actions/Settings/notifications.js b/frontend/src/Store/Actions/Settings/notifications.js index 4e557db87..15943255c 100644 --- a/frontend/src/Store/Actions/Settings/notifications.js +++ b/frontend/src/Store/Actions/Settings/notifications.js @@ -107,8 +107,6 @@ export default { selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport; selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; selectedSchema.onRename = selectedSchema.supportsOnRename; - selectedSchema.onArtistAdd = selectedSchema.supportsOnArtistAdd; - selectedSchema.onArtistDelete = selectedSchema.supportsOnArtistDelete; selectedSchema.onHealthIssue = selectedSchema.supportsOnHealthIssue; selectedSchema.onDownloadFailure = selectedSchema.supportsOnDownloadFailure; selectedSchema.onImportFailure = selectedSchema.supportsOnImportFailure; diff --git a/frontend/src/Store/Actions/albumActions.js b/frontend/src/Store/Actions/albumActions.js index 4ac39a0aa..d1ac2c348 100644 --- a/frontend/src/Store/Actions/albumActions.js +++ b/frontend/src/Store/Actions/albumActions.js @@ -36,11 +36,6 @@ export const defaultState = { sortPredicates: { rating: function(item) { return item.ratings.value; - }, - size: function(item) { - const { statistics = {} } = item; - - return statistics.sizeOnDisk || 0; } }, @@ -86,12 +81,6 @@ export const defaultState = { isSortable: true, isVisible: false }, - { - name: 'size', - label: () => translate('Size'), - isSortable: true, - isVisible: false - }, { name: 'rating', label: () => translate('Rating'), diff --git a/frontend/src/Store/Actions/albumSelectionActions.js b/frontend/src/Store/Actions/albumSelectionActions.js deleted file mode 100644 index f19f5b691..000000000 --- a/frontend/src/Store/Actions/albumSelectionActions.js +++ /dev/null @@ -1,86 +0,0 @@ -import moment from 'moment'; -import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; -import { createThunk, handleThunks } from 'Store/thunks'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import createFetchHandler from './Creators/createFetchHandler'; -import createHandleActions from './Creators/createHandleActions'; -import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; - -// -// Variables - -export const section = 'albumSelection'; - -// -// State - -export const defaultState = { - isFetching: false, - isReprocessing: false, - isPopulated: false, - error: null, - sortKey: 'title', - sortDirection: sortDirections.ASCENDING, - items: [], - sortPredicates: { - title: ({ title }) => { - return title.toLocaleLowerCase(); - }, - - releaseDate: function({ releaseDate }, direction) { - if (releaseDate) { - return moment(releaseDate).unix(); - } - - if (direction === sortDirections.DESCENDING) { - return 0; - } - - return Number.MAX_VALUE; - } - } -}; - -export const persistState = [ - 'albumSelection.sortKey', - 'albumSelection.sortDirection' -]; - -// -// Actions Types - -export const FETCH_ALBUMS = 'albumSelection/fetchAlbums'; -export const SET_ALBUMS_SORT = 'albumSelection/setAlbumsSort'; -export const CLEAR_ALBUMS = 'albumSelection/clearAlbums'; - -// -// Action Creators - -export const fetchAlbums = createThunk(FETCH_ALBUMS); -export const setAlbumsSort = createAction(SET_ALBUMS_SORT); -export const clearAlbums = createAction(CLEAR_ALBUMS); - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - [FETCH_ALBUMS]: createFetchHandler(section, '/album') -}); - -// -// Reducers - -export const reducers = createHandleActions({ - - [SET_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(section), - - [CLEAR_ALBUMS]: (state) => { - return updateSectionState(state, section, { - ...defaultState, - sortKey: state.sortKey, - sortDirection: state.sortDirection - }); - } - -}, defaultState, section); diff --git a/frontend/src/Store/Actions/albumStudioActions.js b/frontend/src/Store/Actions/albumStudioActions.js new file mode 100644 index 000000000..0f543a7d6 --- /dev/null +++ b/frontend/src/Store/Actions/albumStudioActions.js @@ -0,0 +1,167 @@ +import { createAction } from 'redux-actions'; +import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import translate from 'Utilities/String/translate'; +import { fetchAlbums } from './albumActions'; +import { filterPredicates, filters } from './artistActions'; +import { set } from './baseActions'; +import createHandleActions from './Creators/createHandleActions'; +import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; + +// +// Variables + +export const section = 'albumStudio'; + +// +// State + +export const defaultState = { + isSaving: false, + saveError: null, + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + selectedFilterKey: 'all', + filters, + filterPredicates, + + filterBuilderProps: [ + { + name: 'monitored', + label: () => translate('Monitored'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL + }, + { + name: 'status', + label: () => translate('Status'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.ARTIST_STATUS + }, + { + name: 'artistType', + label: () => translate('ArtistType'), + type: filterBuilderTypes.EXACT + }, + { + name: 'qualityProfileId', + label: () => translate('QualityProfile'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.QUALITY_PROFILE + }, + { + name: 'metadataProfileId', + label: () => translate('MetadataProfile'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.METADATA_PROFILE + }, + { + name: 'rootFolderPath', + label: () => translate('RootFolderPath'), + type: filterBuilderTypes.EXACT + }, + { + name: 'tags', + label: () => translate('Tags'), + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.TAG + } + ] +}; + +export const persistState = [ + 'albumStudio.sortKey', + 'albumStudio.sortDirection', + 'albumStudio.selectedFilterKey', + 'albumStudio.customFilters' +]; + +// +// Actions Types + +export const SET_ALBUM_STUDIO_SORT = 'albumStudio/setAlbumStudioSort'; +export const SET_ALBUM_STUDIO_FILTER = 'albumStudio/setAlbumStudioFilter'; +export const SAVE_ALBUM_STUDIO = 'albumStudio/saveAlbumStudio'; + +// +// Action Creators + +export const setAlbumStudioSort = createAction(SET_ALBUM_STUDIO_SORT); +export const setAlbumStudioFilter = createAction(SET_ALBUM_STUDIO_FILTER); +export const saveAlbumStudio = createThunk(SAVE_ALBUM_STUDIO); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [SAVE_ALBUM_STUDIO]: function(getState, payload, dispatch) { + const { + artistIds, + monitored, + monitor, + monitorNewItems + } = payload; + + const artist = []; + + artistIds.forEach((id) => { + const artistToUpdate = { id }; + + if (payload.hasOwnProperty('monitored')) { + artistToUpdate.monitored = monitored; + } + + artist.push(artistToUpdate); + }); + + dispatch(set({ + section, + isSaving: true + })); + + const promise = createAjaxRequest({ + url: '/albumStudio', + method: 'POST', + data: JSON.stringify({ + artist, + monitoringOptions: { monitor }, + monitorNewItems + }), + dataType: 'json' + }).request; + + promise.done((data) => { + dispatch(fetchAlbums()); + + dispatch(set({ + section, + isSaving: false, + saveError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_ALBUM_STUDIO_SORT]: createSetClientSideCollectionSortReducer(section), + [SET_ALBUM_STUDIO_FILTER]: createSetClientSideCollectionFilterReducer(section) + +}, defaultState, section); + diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js index 89384bcc4..ee81624e0 100644 --- a/frontend/src/Store/Actions/artistActions.js +++ b/frontend/src/Store/Actions/artistActions.js @@ -2,12 +2,11 @@ import _ from 'lodash'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; -import { fetchAlbums } from 'Store/Actions/albumActions'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; import translate from 'Utilities/String/translate'; -import { set, updateItem } from './baseActions'; +import { updateItem } from './baseActions'; import createFetchHandler from './Creators/createFetchHandler'; import createHandleActions from './Creators/createHandleActions'; import createRemoveItemHandler from './Creators/createRemoveItemHandler'; @@ -155,21 +154,12 @@ export const defaultState = { error: null, isSaving: false, saveError: null, - isDeleting: false, - deleteError: null, items: [], sortKey: 'sortName', sortDirection: sortDirections.ASCENDING, - pendingChanges: {}, - deleteOptions: { - addImportListExclusion: false - } + pendingChanges: {} }; -export const persistState = [ - 'artist.deleteOptions' -]; - // // Actions Types @@ -180,11 +170,6 @@ export const DELETE_ARTIST = 'artist/deleteArtist'; export const TOGGLE_ARTIST_MONITORED = 'artist/toggleArtistMonitored'; export const TOGGLE_ALBUM_MONITORED = 'artist/toggleAlbumMonitored'; -export const UPDATE_ARTISTS_MONITOR = 'artist/updateArtistsMonitor'; -export const SAVE_ARTIST_EDITOR = 'artist/saveArtistEditor'; -export const BULK_DELETE_ARTIST = 'artist/bulkDeleteArtist'; - -export const SET_DELETE_OPTION = 'artist/setDeleteOption'; // // Action Creators @@ -218,9 +203,6 @@ export const deleteArtist = createThunk(DELETE_ARTIST, (payload) => { export const toggleArtistMonitored = createThunk(TOGGLE_ARTIST_MONITORED); export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED); -export const updateArtistsMonitor = createThunk(UPDATE_ARTISTS_MONITOR); -export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR); -export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST); export const setArtistValue = createAction(SET_ARTIST_VALUE, (payload) => { return { @@ -229,8 +211,6 @@ export const setArtistValue = createAction(SET_ARTIST_VALUE, (payload) => { }; }); -export const setDeleteOption = createAction(SET_DELETE_OPTION); - // // Helpers @@ -351,145 +331,8 @@ export const actionHandlers = handleThunks({ seasons: artist.seasons })); }); - }, - - [UPDATE_ARTISTS_MONITOR]: function(getState, payload, dispatch) { - const { - artistIds, - monitor, - monitored, - monitorNewItems, - shouldFetchAlbumsAfterUpdate = false - } = payload; - - const artists = []; - - artistIds.forEach((id) => { - const artistsToUpdate = { id }; - - if (monitored != null) { - artistsToUpdate.monitored = monitored; - } - - artists.push(artistsToUpdate); - }); - - dispatch(set({ - section, - isSaving: true - })); - - const promise = createAjaxRequest({ - url: '/albumStudio', - method: 'POST', - data: JSON.stringify({ - artist: artists, - monitoringOptions: { monitor }, - monitorNewItems - }), - dataType: 'json' - }).request; - - promise.done((data) => { - if (shouldFetchAlbumsAfterUpdate) { - dispatch(fetchAlbums({ artistId: artistIds[0] })); - } - - dispatch(set({ - section, - isSaving: false, - saveError: null - })); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isSaving: false, - saveError: xhr - })); - }); - }, - - [SAVE_ARTIST_EDITOR]: function(getState, payload, dispatch) { - dispatch(set({ - section, - isSaving: true - })); - - const promise = createAjaxRequest({ - url: '/artist/editor', - method: 'PUT', - data: JSON.stringify(payload), - dataType: 'json' - }).request; - - promise.done((data) => { - dispatch(batchActions([ - ...data.map((artist) => { - - const { - images, - rootFolderPath, - statistics, - ...propsToUpdate - } = artist; - - return updateItem({ - id: artist.id, - section: 'artist', - ...propsToUpdate - }); - }), - - set({ - section, - isSaving: false, - saveError: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isSaving: false, - saveError: xhr - })); - }); - }, - - [BULK_DELETE_ARTIST]: function(getState, payload, dispatch) { - dispatch(set({ - section, - isDeleting: true - })); - - const promise = createAjaxRequest({ - url: '/artist/editor', - method: 'DELETE', - data: JSON.stringify(payload), - dataType: 'json' - }).request; - - promise.done(() => { - // SignaR will take care of removing the artist from the collection - - dispatch(set({ - section, - isDeleting: false, - deleteError: null - })); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isDeleting: false, - deleteError: xhr - })); - }); } + }); // @@ -497,15 +340,6 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ - [SET_ARTIST_VALUE]: createSetSettingValueReducer(section), - - [SET_DELETE_OPTION]: (state, { payload }) => { - return { - ...state, - deleteOptions: { - ...payload - } - }; - } + [SET_ARTIST_VALUE]: createSetSettingValueReducer(section) }, defaultState, section); diff --git a/frontend/src/Store/Actions/artistEditorActions.js b/frontend/src/Store/Actions/artistEditorActions.js new file mode 100644 index 000000000..2e3d5d1f7 --- /dev/null +++ b/frontend/src/Store/Actions/artistEditorActions.js @@ -0,0 +1,251 @@ +import { createAction } from 'redux-actions'; +import { batchActions } from 'redux-batched-actions'; +import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import translate from 'Utilities/String/translate'; +import { filterPredicates, filters, sortPredicates } from './artistActions'; +import { set, updateItem } from './baseActions'; +import createHandleActions from './Creators/createHandleActions'; +import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; + +// +// Variables + +export const section = 'artistEditor'; + +// +// State + +export const defaultState = { + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + selectedFilterKey: 'all', + filters, + filterPredicates, + + columns: [ + { + name: 'status', + columnLabel: () => translate('Status'), + isSortable: true, + isVisible: true, + isModifiable: false + }, + { + name: 'sortName', + label: () => translate('Name'), + isSortable: true, + isVisible: true + }, + { + name: 'monitorNewItems', + label: () => translate('MonitorNewItems'), + isSortable: true, + isVisible: true + }, + { + name: 'qualityProfileId', + label: () => translate('QualityProfile'), + isSortable: true, + isVisible: true + }, + { + name: 'metadataProfileId', + label: () => translate('MetadataProfile'), + isSortable: true, + isVisible: true + }, + { + name: 'path', + label: () => translate('Path'), + isSortable: true, + isVisible: true + }, + { + name: 'sizeOnDisk', + label: () => translate('SizeOnDisk'), + isSortable: true, + isVisible: false + }, + { + name: 'tags', + label: () => translate('Tags'), + isSortable: true, + isVisible: true + } + ], + + filterBuilderProps: [ + { + name: 'monitored', + label: () => translate('Monitored'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL + }, + { + name: 'status', + label: () => translate('Status'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.ARTIST_STATUS + }, + { + name: 'qualityProfileId', + label: () => translate('QualityProfile'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.QUALITY_PROFILE + }, + { + name: 'metadataProfileId', + label: () => translate('MetadataProfile'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.METADATA_PROFILE + }, + { + name: 'path', + label: () => translate('Path'), + type: filterBuilderTypes.STRING + }, + { + name: 'rootFolderPath', + label: () => translate('RootFolderPath'), + type: filterBuilderTypes.EXACT + }, + { + name: 'sizeOnDisk', + label: () => translate('SizeOnDisk'), + type: filterBuilderTypes.NUMBER, + valueType: filterBuilderValueTypes.BYTES + }, + { + name: 'tags', + label: () => translate('Tags'), + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.TAG + } + ], + + sortPredicates +}; + +export const persistState = [ + 'artistEditor.sortKey', + 'artistEditor.sortDirection', + 'artistEditor.selectedFilterKey', + 'artistEditor.customFilters', + 'artistEditor.columns' +]; + +// +// Actions Types + +export const SET_ARTIST_EDITOR_SORT = 'artistEditor/setArtistEditorSort'; +export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter'; +export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor'; +export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist'; +export const SET_ARTIST_EDITOR_TABLE_OPTION = 'artistEditor/setArtistEditorTableOption'; + +// +// Action Creators + +export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT); +export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER); +export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR); +export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST); +export const setArtistEditorTableOption = createAction(SET_ARTIST_EDITOR_TABLE_OPTION); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [SAVE_ARTIST_EDITOR]: function(getState, payload, dispatch) { + dispatch(set({ + section, + isSaving: true + })); + + const promise = createAjaxRequest({ + url: '/artist/editor', + method: 'PUT', + data: JSON.stringify(payload), + dataType: 'json' + }).request; + + promise.done((data) => { + dispatch(batchActions([ + ...data.map((artist) => { + return updateItem({ + id: artist.id, + section: 'artist', + ...artist + }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }, + + [BULK_DELETE_ARTIST]: function(getState, payload, dispatch) { + dispatch(set({ + section, + isDeleting: true + })); + + const promise = createAjaxRequest({ + url: '/artist/editor', + method: 'DELETE', + data: JSON.stringify(payload), + dataType: 'json' + }).request; + + promise.done(() => { + // SignalR will take care of removing the artist from the collection + + dispatch(set({ + section, + isDeleting: false, + deleteError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_ARTIST_EDITOR_TABLE_OPTION]: createSetTableOptionReducer(section), + [SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section), + [SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js index 736502460..e54b38df6 100644 --- a/frontend/src/Store/Actions/artistIndexActions.js +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -1,6 +1,6 @@ import { createAction } from 'redux-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import { filterPredicates, filters, sortPredicates } from './artistActions'; import createHandleActions from './Creators/createHandleActions'; @@ -39,7 +39,6 @@ export const defaultState = { showTitle: false, showMonitored: true, showQualityProfile: true, - showNextAlbum: true, showSearchAction: false }, @@ -94,12 +93,6 @@ export const defaultState = { isSortable: true, isVisible: false }, - { - name: 'monitorNewItems', - label: () => translate('MonitorNewItems'), - isSortable: true, - isVisible: false - }, { name: 'nextAlbum', label: () => translate('NextAlbum'), @@ -151,7 +144,7 @@ export const defaultState = { { name: 'genres', label: () => translate('Genres'), - isSortable: true, + isSortable: false, isVisible: false }, { @@ -182,7 +175,7 @@ export const defaultState = { const { trackCount = 0, - trackFileCount = 0 + trackFileCount } = statistics; const progress = trackCount ? trackFileCount / trackCount * 100 : 100; @@ -207,7 +200,7 @@ export const defaultState = { albumCount: function(item) { const { statistics = {} } = item; - return statistics.albumCount || 0; + return statistics.albumCount; }, trackCount: function(item) { @@ -235,7 +228,7 @@ export const defaultState = { const { trackCount = 0, - trackFileCount = 0 + trackFileCount } = statistics; const progress = trackCount ? @@ -273,12 +266,6 @@ export const defaultState = { type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.METADATA_PROFILE }, - { - name: 'monitorNewItems', - label: () => translate('MonitorNewItems'), - type: filterBuilderTypes.EXACT, - valueType: filterBuilderValueTypes.MONITOR_NEW_ITEMS - }, { name: 'nextAlbum', label: () => translate('NextAlbum'), @@ -334,7 +321,7 @@ export const defaultState = { return acc; }, []); - return tagList.sort(sortByProp('name')); + return tagList.sort(sortByName); } }, { diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index e13ff4672..d473f1368 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -4,10 +4,9 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import * as calendarViews from 'Calendar/calendarViews'; import * as commandNames from 'Commands/commandNames'; -import { filterBuilderTypes, filterBuilderValueTypes, filterTypes } from 'Helpers/Props'; +import { filterTypes } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import findSelectedFilters from 'Utilities/Filter/findSelectedFilters'; import translate from 'Utilities/String/translate'; import { set, update } from './baseActions'; import { executeCommandHelper } from './commandActions'; @@ -55,8 +54,8 @@ export const defaultState = { label: () => translate('All'), filters: [ { - key: 'unmonitored', - value: [true], + key: 'monitored', + value: false, type: filterTypes.EQUAL } ] @@ -66,35 +65,19 @@ export const defaultState = { label: () => translate('MonitoredOnly'), filters: [ { - key: 'unmonitored', - value: [false], + key: 'monitored', + value: true, type: filterTypes.EQUAL } ] } - ], - - filterBuilderProps: [ - { - name: 'unmonitored', - label: () => translate('IncludeUnmonitored'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.BOOL - }, - { - name: 'tags', - label: () => translate('Tags'), - type: filterBuilderTypes.CONTAINS, - valueType: filterBuilderValueTypes.TAG - } ] }; export const persistState = [ 'calendar.view', 'calendar.selectedFilterKey', - 'calendar.options', - 'calendar.customFilters' + 'calendar.options' ]; // @@ -206,10 +189,6 @@ function isRangePopulated(start, end, state) { return false; } -function getCustomFilters(state, type) { - return state.customFilters.items.filter((customFilter) => customFilter.type === type); -} - // // Action Creators @@ -231,8 +210,7 @@ export const actionHandlers = handleThunks({ [FETCH_CALENDAR]: function(getState, payload, dispatch) { const state = getState(); const calendar = state.calendar; - const customFilters = getCustomFilters(state, section); - const selectedFilters = findSelectedFilters(calendar.selectedFilterKey, calendar.filters, customFilters); + const unmonitored = calendar.selectedFilterKey === 'all'; const { time = calendar.time, @@ -259,26 +237,13 @@ export const actionHandlers = handleThunks({ dispatch(set(attrs)); - const requestParams = { - start, - end - }; - - selectedFilters.forEach((selectedFilter) => { - if (selectedFilter.key === 'unmonitored') { - requestParams.unmonitored = selectedFilter.value.includes(true); - } - - if (selectedFilter.key === 'tags') { - requestParams.tags = selectedFilter.value.join(','); - } - }); - - requestParams.unmonitored = requestParams.unmonitored ?? false; - const promise = createAjaxRequest({ url: '/calendar', - data: requestParams + data: { + unmonitored, + start, + end + } }).request; promise.done((data) => { diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 9d16d29c4..9dc212645 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -1,7 +1,7 @@ import React from 'react'; import { createAction } from 'redux-actions'; import Icon from 'Components/Icon'; -import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props'; +import { filterTypes, icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -60,7 +60,7 @@ export const defaultState = { }, { name: 'customFormats', - label: () => translate('Formats'), + label: 'Formats', isSortable: false, isVisible: true }, @@ -90,12 +90,17 @@ export const defaultState = { label: () => translate('SourceTitle'), isVisible: false }, + { + name: 'sourceTitle', + label: 'Source Title', + isVisible: false + }, { name: 'customFormatScore', - columnLabel: () => translate('CustomFormatScore'), + columnLabel: 'Custom Format Score', label: React.createElement(Icon, { name: icons.SCORE, - title: () => translate('CustomFormatScore') + title: 'Custom format score' }), isVisible: false }, @@ -150,7 +155,7 @@ export const defaultState = { }, { key: 'importFailed', - label: () => translate('ImportCompleteFailed'), + label: () => translate('ImportFailed'), filters: [ { key: 'eventType', @@ -214,27 +219,6 @@ export const defaultState = { } ] } - ], - - filterBuilderProps: [ - { - name: 'eventType', - label: () => translate('EventType'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE - }, - { - name: 'artistIds', - label: () => translate('Artist'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.ARTIST - }, - { - name: 'quality', - label: () => translate('Quality'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.QUALITY - } ] }; diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 85fda482b..577c19c6b 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -1,8 +1,9 @@ import * as albums from './albumActions'; import * as albumHistory from './albumHistoryActions'; -import * as albumSelection from './albumSelectionActions'; +import * as albumStudio from './albumStudioActions'; import * as app from './appActions'; import * as artist from './artistActions'; +import * as artistEditor from './artistEditorActions'; import * as artistHistory from './artistHistoryActions'; import * as artistIndex from './artistIndexActions'; import * as blocklist from './blocklistActions'; @@ -14,7 +15,6 @@ import * as history from './historyActions'; import * as interactiveImportActions from './interactiveImportActions'; import * as oAuth from './oAuthActions'; import * as organizePreview from './organizePreviewActions'; -import * as parse from './parseActions'; import * as paths from './pathActions'; import * as providerOptions from './providerOptionActions'; import * as queue from './queueActions'; @@ -30,28 +30,28 @@ import * as wanted from './wantedActions'; export default [ app, - albums, - albumHistory, - albumSelection, - artist, - artistHistory, - artistIndex, blocklist, captcha, calendar, commands, customFilters, + albums, trackFiles, + albumHistory, history, interactiveImportActions, oAuth, organizePreview, retagPreview, - parse, paths, providerOptions, queue, releases, + albumStudio, + artist, + artistEditor, + artistHistory, + artistIndex, search, settings, system, diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index a250292c5..e9008bb85 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -16,8 +16,8 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create export const section = 'interactiveImport'; +const albumsSection = `${section}.albums`; const trackFilesSection = `${section}.trackFiles`; -let abortCurrentFetchRequest = null; let abortCurrentRequest = null; let currentIds = []; @@ -35,8 +35,6 @@ export const defaultState = { pendingChanges: {}, sortKey: 'path', sortDirection: sortDirections.ASCENDING, - secondarySortKey: 'path', - secondarySortDirection: sortDirections.ASCENDING, recentFolders: [], importMode: 'chooseImportMode', sortPredicates: { @@ -57,6 +55,15 @@ export const defaultState = { } }, + albums: { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'albumTitle', + sortDirection: sortDirections.ASCENDING, + items: [] + }, + trackFiles: { isFetching: false, isPopulated: false, @@ -68,8 +75,6 @@ export const defaultState = { }; export const persistState = [ - 'interactiveImport.sortKey', - 'interactiveImport.sortDirection', 'interactiveImport.recentFolders', 'interactiveImport.importMode' ]; @@ -87,6 +92,10 @@ export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder'; export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder'; export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode'; +export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/fetchInteractiveImportAlbums'; +export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'interactiveImport/clearInteractiveImportAlbumsSort'; +export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/clearInteractiveImportAlbums'; + export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles'; export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles'; @@ -103,6 +112,10 @@ export const addRecentFolder = createAction(ADD_RECENT_FOLDER); export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER); export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE); +export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT_ALBUMS); +export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT); +export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS); + export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES); export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES); @@ -110,11 +123,6 @@ export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_I // Action Handlers export const actionHandlers = handleThunks({ [FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) { - if (abortCurrentFetchRequest) { - abortCurrentFetchRequest(); - abortCurrentFetchRequest = null; - } - if (!payload.downloadId && !payload.folder) { dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } })); return; @@ -122,14 +130,12 @@ export const actionHandlers = handleThunks({ dispatch(set({ section, isFetching: true })); - const { request, abortRequest } = createAjaxRequest({ + const promise = createAjaxRequest({ url: '/manualimport', data: payload - }); + }).request; - abortCurrentFetchRequest = abortRequest; - - request.done((data) => { + promise.done((data) => { dispatch(batchActions([ update({ section, data }), @@ -142,11 +148,7 @@ export const actionHandlers = handleThunks({ ])); }); - request.fail((xhr) => { - if (xhr.aborted) { - return; - } - + promise.fail((xhr) => { dispatch(set({ section, isFetching: false, @@ -190,7 +192,6 @@ export const actionHandlers = handleThunks({ trackIds: (item.tracks || []).map((e) => e.id), quality: item.quality, releaseGroup: item.releaseGroup, - indexerFlags: item.indexerFlags, downloadId: item.downloadId, additionalFile: item.additionalFile, replaceExistingFiles: item.replaceExistingFiles, @@ -235,6 +236,8 @@ export const actionHandlers = handleThunks({ }); }, + [FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'), + [FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile') }); @@ -316,6 +319,14 @@ export const reducers = createHandleActions({ return Object.assign({}, state, { importMode: payload.importMode }); }, + [SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(albumsSection), + + [CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => { + return updateSectionState(state, albumsSection, { + ...defaultState.albums + }); + }, + [CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => { return updateSectionState(state, trackFilesSection, { ...defaultState.trackFiles diff --git a/frontend/src/Store/Actions/parseActions.ts b/frontend/src/Store/Actions/parseActions.ts deleted file mode 100644 index d4b6e9bcb..000000000 --- a/frontend/src/Store/Actions/parseActions.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Dispatch } from 'redux'; -import { createAction } from 'redux-actions'; -import { batchActions } from 'redux-batched-actions'; -import AppState from 'App/State/AppState'; -import { createThunk, handleThunks } from 'Store/thunks'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import { set, update } from './baseActions'; -import createHandleActions from './Creators/createHandleActions'; -import createClearReducer from './Creators/Reducers/createClearReducer'; - -interface FetchPayload { - title: string; -} - -// -// Variables - -export const section = 'parse'; -let parseTimeout: number | null = null; -let abortCurrentRequest: (() => void) | null = null; - -// -// State - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - item: {}, -}; - -// -// Actions Types - -export const FETCH = 'parse/fetch'; -export const CLEAR = 'parse/clear'; - -// -// Action Creators - -export const fetch = createThunk(FETCH); -export const clear = createAction(CLEAR); - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - [FETCH]: function ( - _getState: () => AppState, - payload: FetchPayload, - dispatch: Dispatch - ) { - if (parseTimeout) { - clearTimeout(parseTimeout); - } - - parseTimeout = window.setTimeout(async () => { - dispatch(set({ section, isFetching: true })); - - if (abortCurrentRequest) { - abortCurrentRequest(); - } - - const { request, abortRequest } = createAjaxRequest({ - url: '/parse', - data: { - title: payload.title, - }, - }); - - try { - const data = await request; - - dispatch( - batchActions([ - update({ section, data }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null, - }), - ]) - ); - } catch (error) { - dispatch( - set({ - section, - isAdding: false, - isAdded: false, - addError: error, - }) - ); - } - - abortCurrentRequest = abortRequest; - }, 300); - }, -}); - -// -// Reducers - -export const reducers = createHandleActions( - { - [CLEAR]: createClearReducer(section, defaultState), - }, - defaultState, - section -); diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index 4bf200a5c..dc20da73d 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -3,7 +3,7 @@ import React from 'react'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import Icon from 'Components/Icon'; -import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props'; +import { icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -132,7 +132,7 @@ export const defaultState = { name: 'size', label: () => translate('Size'), isSortable: true, - isVisible: false + isVisibile: false }, { name: 'outputPath', @@ -146,12 +146,6 @@ export const defaultState = { isSortable: true, isVisible: true }, - { - name: 'added', - label: () => translate('Added'), - isSortable: true, - isVisible: false - }, { name: 'progress', label: () => translate('Progress'), @@ -164,37 +158,6 @@ export const defaultState = { isVisible: true, isModifiable: false } - ], - - selectedFilterKey: 'all', - - filters: [ - { - key: 'all', - label: 'All', - filters: [] - } - ], - - filterBuilderProps: [ - { - name: 'artistIds', - label: () => translate('Artist'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.ARTIST - }, - { - name: 'quality', - label: () => translate('Quality'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.QUALITY - }, - { - name: 'protocol', - label: () => translate('Protocol'), - type: filterBuilderTypes.EQUAL, - valueType: filterBuilderValueTypes.PROTOCOL - } ] } }; @@ -204,8 +167,7 @@ export const persistState = [ 'queue.paged.pageSize', 'queue.paged.sortKey', 'queue.paged.sortDirection', - 'queue.paged.columns', - 'queue.paged.selectedFilterKey' + 'queue.paged.columns' ]; // @@ -230,7 +192,6 @@ export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage'; export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage'; export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage'; export const SET_QUEUE_SORT = 'queue/setQueueSort'; -export const SET_QUEUE_FILTER = 'queue/setQueueFilter'; export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption'; export const SET_QUEUE_OPTION = 'queue/setQueueOption'; export const CLEAR_QUEUE = 'queue/clearQueue'; @@ -255,7 +216,6 @@ export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE); export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE); export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE); export const setQueueSort = createThunk(SET_QUEUE_SORT); -export const setQueueFilter = createThunk(SET_QUEUE_FILTER); export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION); export const setQueueOption = createAction(SET_QUEUE_OPTION); export const clearQueue = createAction(CLEAR_QUEUE); @@ -307,8 +267,7 @@ export const actionHandlers = handleThunks({ [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE, [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE, [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE, - [serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT, - [serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER + [serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT }, fetchDataAugmenter ), @@ -412,14 +371,13 @@ export const actionHandlers = handleThunks({ id, removeFromClient, blocklist, - skipRedownload, - changeCategory + skipRedownload } = payload; dispatch(updateItem({ section: paged, id, isRemoving: true })); const promise = createAjaxRequest({ - url: `/queue/${id}?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`, + url: `/queue/${id}?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, method: 'DELETE' }).request; @@ -437,8 +395,7 @@ export const actionHandlers = handleThunks({ ids, removeFromClient, blocklist, - skipRedownload, - changeCategory + skipRedownload } = payload; dispatch(batchActions([ @@ -454,7 +411,7 @@ export const actionHandlers = handleThunks({ ])); const promise = createAjaxRequest({ - url: `/queue/bulk?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`, + url: `/queue/bulk?removeFromClient=${removeFromClient}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, method: 'DELETE', dataType: 'json', contentType: 'application/json', diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index c4955c915..1c9b6f5ef 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -219,9 +219,8 @@ export const defaultState = { }; export const persistState = [ - 'releases.album.selectedFilterKey', + 'releases.selectedFilterKey', 'releases.album.customFilters', - 'releases.artist.selectedFilterKey', 'releases.artist.customFilters' ]; diff --git a/frontend/src/Store/Actions/searchActions.js b/frontend/src/Store/Actions/searchActions.js index c2af6f47b..c72cdc901 100644 --- a/frontend/src/Store/Actions/searchActions.js +++ b/frontend/src/Store/Actions/searchActions.js @@ -36,8 +36,6 @@ export const defaultState = { monitorNewItems: monitorNewItemsOptions[0].key, qualityProfileId: 0, metadataProfileId: 0, - searchForMissingAlbums: false, - searchForNewAlbum: false, tags: [] } }; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 54b059083..a8af15174 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -1,8 +1,6 @@ import { createAction } from 'redux-actions'; import { handleThunks } from 'Store/thunks'; import createHandleActions from './Creators/createHandleActions'; -import autoTaggings from './Settings/autoTaggings'; -import autoTaggingSpecifications from './Settings/autoTaggingSpecifications'; import customFormats from './Settings/customFormats'; import customFormatSpecifications from './Settings/customFormatSpecifications'; import delayProfiles from './Settings/delayProfiles'; @@ -11,7 +9,6 @@ import downloadClients from './Settings/downloadClients'; import general from './Settings/general'; import importListExclusions from './Settings/importListExclusions'; import importLists from './Settings/importLists'; -import indexerFlags from './Settings/indexerFlags'; import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; import languages from './Settings/languages'; @@ -29,8 +26,6 @@ import remotePathMappings from './Settings/remotePathMappings'; import rootFolders from './Settings/rootFolders'; import ui from './Settings/ui'; -export * from './Settings/autoTaggingSpecifications'; -export * from './Settings/autoTaggings'; export * from './Settings/customFormatSpecifications.js'; export * from './Settings/customFormats'; export * from './Settings/delayProfiles'; @@ -39,7 +34,6 @@ export * from './Settings/downloadClientOptions'; export * from './Settings/general'; export * from './Settings/importLists'; export * from './Settings/importListExclusions'; -export * from './Settings/indexerFlags'; export * from './Settings/indexerOptions'; export * from './Settings/indexers'; export * from './Settings/languages'; @@ -67,15 +61,13 @@ export const section = 'settings'; export const defaultState = { advancedSettings: false, - autoTaggingSpecifications: autoTaggingSpecifications.defaultState, - autoTaggings: autoTaggings.defaultState, + customFormatSpecifications: customFormatSpecifications.defaultState, customFormats: customFormats.defaultState, delayProfiles: delayProfiles.defaultState, downloadClients: downloadClients.defaultState, downloadClientOptions: downloadClientOptions.defaultState, general: general.defaultState, - indexerFlags: indexerFlags.defaultState, indexerOptions: indexerOptions.defaultState, indexers: indexers.defaultState, importLists: importLists.defaultState, @@ -114,15 +106,12 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS); // Action Handlers export const actionHandlers = handleThunks({ - ...autoTaggingSpecifications.actionHandlers, - ...autoTaggings.actionHandlers, ...customFormatSpecifications.actionHandlers, ...customFormats.actionHandlers, ...delayProfiles.actionHandlers, ...downloadClients.actionHandlers, ...downloadClientOptions.actionHandlers, ...general.actionHandlers, - ...indexerFlags.actionHandlers, ...indexerOptions.actionHandlers, ...indexers.actionHandlers, ...importLists.actionHandlers, @@ -152,15 +141,12 @@ export const reducers = createHandleActions({ return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); }, - ...autoTaggingSpecifications.reducers, - ...autoTaggings.reducers, ...customFormatSpecifications.reducers, ...customFormats.reducers, ...delayProfiles.reducers, ...downloadClients.reducers, ...downloadClientOptions.reducers, ...general.reducers, - ...indexerFlags.reducers, ...indexerOptions.reducers, ...indexers.reducers, ...importLists.reducers, diff --git a/frontend/src/Store/Actions/trackActions.js b/frontend/src/Store/Actions/trackActions.js index a71388c88..3522939eb 100644 --- a/frontend/src/Store/Actions/trackActions.js +++ b/frontend/src/Store/Actions/trackActions.js @@ -58,11 +58,6 @@ export const defaultState = { label: () => translate('AudioInfo'), isVisible: true }, - { - name: 'size', - label: () => translate('Size'), - isVisible: false - }, { name: 'customFormats', label: 'Formats', @@ -77,15 +72,6 @@ export const defaultState = { }), isVisible: false }, - { - name: 'indexerFlags', - columnLabel: () => translate('IndexerFlags'), - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), - isVisible: false - }, { name: 'status', label: () => translate('Status'), diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index 61d6f7752..35aa162d4 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -52,12 +52,6 @@ export const defaultState = { isSortable: true, isVisible: true }, - { - name: 'albums.lastSearchTime', - label: () => translate('LastSearched'), - isSortable: true, - isVisible: false - }, // { // name: 'status', // label: 'Status', @@ -137,12 +131,6 @@ export const defaultState = { // label: 'Status', // isVisible: true // }, - { - name: 'albums.lastSearchTime', - label: () => translate('LastSearched'), - isSortable: true, - isVisible: false - }, { name: 'actions', columnLabel: () => translate('Actions'), diff --git a/frontend/src/Store/Selectors/createAllArtistSelector.ts b/frontend/src/Store/Selectors/createAllArtistSelector.js similarity index 71% rename from frontend/src/Store/Selectors/createAllArtistSelector.ts rename to frontend/src/Store/Selectors/createAllArtistSelector.js index 6b6010429..38b1bcef1 100644 --- a/frontend/src/Store/Selectors/createAllArtistSelector.ts +++ b/frontend/src/Store/Selectors/createAllArtistSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createAllArtistSelector() { return createSelector( - (state: AppState) => state.artist, + (state) => state.artist, (artist) => { return artist.items; } diff --git a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts deleted file mode 100644 index 414a451f5..000000000 --- a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSelector } from 'reselect'; -import AlbumAppState from 'App/State/AlbumAppState'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import { createArtistSelectorForHook } from './createArtistSelector'; - -function createArtistAlbumsSelector(artistId: number) { - return createSelector( - (state: AppState) => state.albums, - createArtistSelectorForHook(artistId), - (albums: AlbumAppState, artist = {} as Artist) => { - const { isFetching, isPopulated, error, items } = albums; - - const filteredAlbums = items.filter( - (album) => album.artistId === artist.id - ); - - return { - isFetching, - isPopulated, - error, - items: filteredAlbums, - }; - } - ); -} - -export default createArtistAlbumsSelector; diff --git a/frontend/src/Store/Selectors/createArtistCountSelector.ts b/frontend/src/Store/Selectors/createArtistCountSelector.js similarity index 65% rename from frontend/src/Store/Selectors/createArtistCountSelector.ts rename to frontend/src/Store/Selectors/createArtistCountSelector.js index b432d64a7..31e0a39fc 100644 --- a/frontend/src/Store/Selectors/createArtistCountSelector.ts +++ b/frontend/src/Store/Selectors/createArtistCountSelector.js @@ -1,19 +1,18 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import createAllArtistSelector from './createAllArtistSelector'; function createArtistCountSelector() { return createSelector( createAllArtistSelector(), - (state: AppState) => state.artist.error, - (state: AppState) => state.artist.isFetching, - (state: AppState) => state.artist.isPopulated, + (state) => state.artist.error, + (state) => state.artist.isFetching, + (state) => state.artist.isPopulated, (artists, error, isFetching, isPopulated) => { return { count: artists.length, error, isFetching, - isPopulated, + isPopulated }; } ); diff --git a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.js b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.js new file mode 100644 index 000000000..de5205948 --- /dev/null +++ b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.js @@ -0,0 +1,16 @@ +import { createSelector } from 'reselect'; +import createArtistSelector from './createArtistSelector'; + +function createArtistMetadataProfileSelector() { + return createSelector( + (state) => state.settings.metadataProfiles.items, + createArtistSelector(), + (metadataProfiles, artist = {}) => { + return metadataProfiles.find((profile) => { + return profile.id === artist.metadataProfileId; + }); + } + ); +} + +export default createArtistMetadataProfileSelector; diff --git a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts deleted file mode 100644 index fa60d936d..000000000 --- a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import MetadataProfile from 'typings/MetadataProfile'; -import { createArtistSelectorForHook } from './createArtistSelector'; - -function createArtistMetadataProfileSelector(artistId: number) { - return createSelector( - (state: AppState) => state.settings.metadataProfiles.items, - createArtistSelectorForHook(artistId), - (metadataProfiles: MetadataProfile[], artist = {} as Artist) => { - return metadataProfiles.find((profile) => { - return profile.id === artist.metadataProfileId; - }); - } - ); -} - -export default createArtistMetadataProfileSelector; diff --git a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.js b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.js new file mode 100644 index 000000000..5819eb080 --- /dev/null +++ b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.js @@ -0,0 +1,16 @@ +import { createSelector } from 'reselect'; +import createArtistSelector from './createArtistSelector'; + +function createArtistQualityProfileSelector() { + return createSelector( + (state) => state.settings.qualityProfiles.items, + createArtistSelector(), + (qualityProfiles, artist = {}) => { + return qualityProfiles.find((profile) => { + return profile.id === artist.qualityProfileId; + }); + } + ); +} + +export default createArtistQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts deleted file mode 100644 index 67639919b..000000000 --- a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import QualityProfile from 'typings/QualityProfile'; -import { createArtistSelectorForHook } from './createArtistSelector'; - -function createArtistQualityProfileSelector(artistId: number) { - return createSelector( - (state: AppState) => state.settings.qualityProfiles.items, - createArtistSelectorForHook(artistId), - (qualityProfiles: QualityProfile[], artist = {} as Artist) => { - return qualityProfiles.find( - (profile) => profile.id === artist.qualityProfileId - ); - } - ); -} - -export default createArtistQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createArtistSelector.js b/frontend/src/Store/Selectors/createArtistSelector.js index c335f37f5..104ef83e3 100644 --- a/frontend/src/Store/Selectors/createArtistSelector.js +++ b/frontend/src/Store/Selectors/createArtistSelector.js @@ -1,15 +1,5 @@ import { createSelector } from 'reselect'; -export function createArtistSelectorForHook(artistId) { - return createSelector( - (state) => state.artist.itemMap, - (state) => state.artist.items, - (itemMap, allArtists) => { - return artistId ? allArtists[itemMap[artistId]]: undefined; - } - ); -} - function createArtistSelector() { return createSelector( (state, { artistId }) => artistId, diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js index 1bac14f08..ae1031dca 100644 --- a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -108,7 +108,7 @@ function sort(items, state) { return _.orderBy(items, clauses, orders); } -export function createCustomFiltersSelector(type, alternateType) { +function createCustomFiltersSelector(type, alternateType) { return createSelector( (state) => state.customFilters.items, (customFilters) => { diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts b/frontend/src/Store/Selectors/createCommandExecutingSelector.js similarity index 50% rename from frontend/src/Store/Selectors/createCommandExecutingSelector.ts rename to frontend/src/Store/Selectors/createCommandExecutingSelector.js index 6a80e172b..6037d5820 100644 --- a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts +++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.js @@ -2,10 +2,13 @@ import { createSelector } from 'reselect'; import { isCommandExecuting } from 'Utilities/Command'; import createCommandSelector from './createCommandSelector'; -function createCommandExecutingSelector(name: string, contraints = {}) { - return createSelector(createCommandSelector(name, contraints), (command) => { - return isCommandExecuting(command); - }); +function createCommandExecutingSelector(name, contraints = {}) { + return createSelector( + createCommandSelector(name, contraints), + (command) => { + return isCommandExecuting(command); + } + ); } export default createCommandExecutingSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.js b/frontend/src/Store/Selectors/createCommandSelector.js new file mode 100644 index 000000000..709dfebaf --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandSelector.js @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import createCommandsSelector from './createCommandsSelector'; + +function createCommandSelector(name, contraints = {}) { + return createSelector( + createCommandsSelector(), + (commands) => { + return findCommand(commands, { name, ...contraints }); + } + ); +} + +export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.ts b/frontend/src/Store/Selectors/createCommandSelector.ts deleted file mode 100644 index cced7b186..000000000 --- a/frontend/src/Store/Selectors/createCommandSelector.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createSelector } from 'reselect'; -import { findCommand } from 'Utilities/Command'; -import createCommandsSelector from './createCommandsSelector'; - -function createCommandSelector(name: string, contraints = {}) { - return createSelector(createCommandsSelector(), (commands) => { - return findCommand(commands, { name, ...contraints }); - }); -} - -export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandsSelector.ts b/frontend/src/Store/Selectors/createCommandsSelector.js similarity index 71% rename from frontend/src/Store/Selectors/createCommandsSelector.ts rename to frontend/src/Store/Selectors/createCommandsSelector.js index 2dd5d24a2..7b9edffd9 100644 --- a/frontend/src/Store/Selectors/createCommandsSelector.ts +++ b/frontend/src/Store/Selectors/createCommandsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createCommandsSelector() { return createSelector( - (state: AppState) => state.commands, + (state) => state.commands, (commands) => { return commands.items; } diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.js b/frontend/src/Store/Selectors/createDeepEqualSelector.js new file mode 100644 index 000000000..85562f28b --- /dev/null +++ b/frontend/src/Store/Selectors/createDeepEqualSelector.js @@ -0,0 +1,9 @@ +import _ from 'lodash'; +import { createSelectorCreator, defaultMemoize } from 'reselect'; + +const createDeepEqualSelector = createSelectorCreator( + defaultMemoize, + _.isEqual +); + +export default createDeepEqualSelector; diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.ts b/frontend/src/Store/Selectors/createDeepEqualSelector.ts deleted file mode 100644 index 9d4a63d2e..000000000 --- a/frontend/src/Store/Selectors/createDeepEqualSelector.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { isEqual } from 'lodash'; -import { createSelectorCreator, defaultMemoize } from 'reselect'; - -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual); - -export default createDeepEqualSelector; diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.ts b/frontend/src/Store/Selectors/createDimensionsSelector.js similarity index 69% rename from frontend/src/Store/Selectors/createDimensionsSelector.ts rename to frontend/src/Store/Selectors/createDimensionsSelector.js index b9602cb02..ce26b2e2c 100644 --- a/frontend/src/Store/Selectors/createDimensionsSelector.ts +++ b/frontend/src/Store/Selectors/createDimensionsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createDimensionsSelector() { return createSelector( - (state: AppState) => state.app.dimensions, + (state) => state.app.dimensions, (dimensions) => { return dimensions; } diff --git a/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js similarity index 78% rename from frontend/src/Store/Selectors/createExecutingCommandsSelector.ts rename to frontend/src/Store/Selectors/createExecutingCommandsSelector.js index dd16571fc..266865a8a 100644 --- a/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts +++ b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import { isCommandExecuting } from 'Utilities/Command'; function createExecutingCommandsSelector() { return createSelector( - (state: AppState) => state.commands.items, + (state) => state.commands.items, (commands) => { return commands.filter((command) => isCommandExecuting(command)); } diff --git a/frontend/src/Store/Selectors/createExistingArtistSelector.ts b/frontend/src/Store/Selectors/createExistingArtistSelector.js similarity index 58% rename from frontend/src/Store/Selectors/createExistingArtistSelector.ts rename to frontend/src/Store/Selectors/createExistingArtistSelector.js index 91b5bc4d6..4811f2034 100644 --- a/frontend/src/Store/Selectors/createExistingArtistSelector.ts +++ b/frontend/src/Store/Selectors/createExistingArtistSelector.js @@ -1,15 +1,13 @@ -import { some } from 'lodash'; +import _ from 'lodash'; import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import createAllArtistSelector from './createAllArtistSelector'; function createExistingArtistSelector() { return createSelector( - (_: AppState, { foreignArtistId }: { foreignArtistId: string }) => - foreignArtistId, + (state, { foreignArtistId }) => foreignArtistId, createAllArtistSelector(), (foreignArtistId, artist) => { - return some(artist, { foreignArtistId }); + return _.some(artist, { foreignArtistId }); } ); } diff --git a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts deleted file mode 100644 index 90587639c..000000000 --- a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -const createIndexerFlagsSelector = createSelector( - (state: AppState) => state.settings.indexerFlags, - (indexerFlags) => indexerFlags -); - -export default createIndexerFlagsSelector; diff --git a/frontend/src/Store/Selectors/createMetadataProfileSelector.js b/frontend/src/Store/Selectors/createMetadataProfileSelector.js new file mode 100644 index 000000000..bdd0d0636 --- /dev/null +++ b/frontend/src/Store/Selectors/createMetadataProfileSelector.js @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect'; + +function createMetadataProfileSelector() { + return createSelector( + (state, { metadataProfileId }) => metadataProfileId, + (state) => state.settings.metadataProfiles.items, + (metadataProfileId, metadataProfiles) => { + return metadataProfiles.find((profile) => { + return profile.id === metadataProfileId; + }); + } + ); +} + +export default createMetadataProfileSelector; diff --git a/frontend/src/Store/Selectors/createMetadataProfileSelector.ts b/frontend/src/Store/Selectors/createMetadataProfileSelector.ts deleted file mode 100644 index ae4c061db..000000000 --- a/frontend/src/Store/Selectors/createMetadataProfileSelector.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -function createMetadataProfileSelector() { - return createSelector( - (_: AppState, { metadataProfileId }: { metadataProfileId: number }) => - metadataProfileId, - (state: AppState) => state.settings.metadataProfiles.items, - (metadataProfileId, metadataProfiles) => { - return metadataProfiles.find( - (profile) => profile.id === metadataProfileId - ); - } - ); -} - -export default createMetadataProfileSelector; diff --git a/frontend/src/Store/Selectors/createMultiArtistsSelector.ts b/frontend/src/Store/Selectors/createMultiArtistsSelector.ts deleted file mode 100644 index d8f7ea92b..000000000 --- a/frontend/src/Store/Selectors/createMultiArtistsSelector.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; - -function createMultiArtistsSelector(artistIds: number[]) { - return createSelector( - (state: AppState) => state.artist.itemMap, - (state: AppState) => state.artist.items, - (itemMap, allArtists) => { - return artistIds.reduce((acc: Artist[], artistId) => { - const artist = allArtists[itemMap[artistId]]; - - if (artist) { - acc.push(artist); - } - - return acc; - }, []); - } - ); -} - -export default createMultiArtistsSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js new file mode 100644 index 000000000..84fefb83e --- /dev/null +++ b/frontend/src/Store/Selectors/createProfileInUseSelector.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from './createAllArtistSelector'; + +function createProfileInUseSelector(profileProp) { + return createSelector( + (state, { id }) => id, + createAllArtistSelector(), + (state) => state.settings.importLists.items, + (id, artist, lists) => { + if (!id) { + return false; + } + + if (_.some(artist, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) { + return true; + } + + return false; + } + ); +} + +export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.ts b/frontend/src/Store/Selectors/createProfileInUseSelector.ts deleted file mode 100644 index 85f0c3211..000000000 --- a/frontend/src/Store/Selectors/createProfileInUseSelector.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Artist from 'Artist/Artist'; -import ImportList from 'typings/ImportList'; -import createAllArtistSelector from './createAllArtistSelector'; - -function createProfileInUseSelector(profileProp: string) { - return createSelector( - (_: AppState, { id }: { id: number }) => id, - createAllArtistSelector(), - (state: AppState) => state.settings.importLists.items, - (id, artists, lists) => { - if (!id) { - return false; - } - - return ( - artists.some((a) => a[profileProp as keyof Artist] === id) || - lists.some((list) => list[profileProp as keyof ImportList] === id) - ); - } - ); -} - -export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createProviderSettingsSelector.js b/frontend/src/Store/Selectors/createProviderSettingsSelector.js index f5ac9bad5..46659609f 100644 --- a/frontend/src/Store/Selectors/createProviderSettingsSelector.js +++ b/frontend/src/Store/Selectors/createProviderSettingsSelector.js @@ -2,70 +2,62 @@ import _ from 'lodash'; import { createSelector } from 'reselect'; import selectSettings from 'Store/Selectors/selectSettings'; -function selector(id, section) { - if (!id) { - const item = _.isArray(section.schema) ? section.selectedSchema : section.schema; - const settings = selectSettings(Object.assign({ name: '' }, item), section.pendingChanges, section.saveError); - - const { - isSchemaFetching: isFetching, - isSchemaPopulated: isPopulated, - schemaError: error, - isSaving, - saveError, - isTesting, - pendingChanges - } = section; - - return { - isFetching, - isPopulated, - error, - isSaving, - saveError, - isTesting, - pendingChanges, - ...settings, - item: settings.settings - }; - } - - const { - isFetching, - isPopulated, - error, - isSaving, - saveError, - isTesting, - pendingChanges - } = section; - - const settings = selectSettings(_.find(section.items, { id }), pendingChanges, saveError); - - return { - isFetching, - isPopulated, - error, - isSaving, - saveError, - isTesting, - ...settings, - item: settings.settings - }; -} - -export default function createProviderSettingsSelector(sectionName) { +function createProviderSettingsSelector(sectionName) { return createSelector( (state, { id }) => id, (state) => state.settings[sectionName], - (id, section) => selector(id, section) - ); -} - -export function createProviderSettingsSelectorHook(sectionName, id) { - return createSelector( - (state) => state.settings[sectionName], - (section) => selector(id, section) + (id, section) => { + if (!id) { + const item = _.isArray(section.schema) ? section.selectedSchema : section.schema; + const settings = selectSettings(Object.assign({ name: '' }, item), section.pendingChanges, section.saveError); + + const { + isSchemaFetching: isFetching, + isSchemaPopulated: isPopulated, + schemaError: error, + isSaving, + saveError, + isTesting, + pendingChanges + } = section; + + return { + isFetching, + isPopulated, + error, + isSaving, + saveError, + isTesting, + pendingChanges, + ...settings, + item: settings.settings + }; + } + + const { + isFetching, + isPopulated, + error, + isSaving, + saveError, + isTesting, + pendingChanges + } = section; + + const settings = selectSettings(_.find(section.items, { id }), pendingChanges, saveError); + + return { + isFetching, + isPopulated, + error, + isSaving, + saveError, + isTesting, + ...settings, + item: settings.settings + }; + } ); } +export default createProviderSettingsSelector; diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.js b/frontend/src/Store/Selectors/createQualityProfileSelector.js new file mode 100644 index 000000000..611dfc903 --- /dev/null +++ b/frontend/src/Store/Selectors/createQualityProfileSelector.js @@ -0,0 +1,26 @@ +import { createSelector } from 'reselect'; + +export function createQualityProfileSelectorForHook(qualityProfileId) { + return createSelector( + (state) => state.settings.qualityProfiles.items, + (qualityProfiles) => { + return qualityProfiles.find((profile) => { + return profile.id === qualityProfileId; + }); + } + ); +} + +function createQualityProfileSelector() { + return createSelector( + (state, { qualityProfileId }) => qualityProfileId, + (state) => state.settings.qualityProfiles.items, + (qualityProfileId, qualityProfiles) => { + return qualityProfiles.find((profile) => { + return profile.id === qualityProfileId; + }); + } + ); +} + +export default createQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.ts b/frontend/src/Store/Selectors/createQualityProfileSelector.ts deleted file mode 100644 index b913e0c46..000000000 --- a/frontend/src/Store/Selectors/createQualityProfileSelector.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -export function createQualityProfileSelectorForHook(qualityProfileId: number) { - return createSelector( - (state: AppState) => state.settings.qualityProfiles.items, - (qualityProfiles) => { - return qualityProfiles.find((profile) => profile.id === qualityProfileId); - } - ); -} - -function createQualityProfileSelector() { - return createSelector( - (_: AppState, { qualityProfileId }: { qualityProfileId: number }) => - qualityProfileId, - (state: AppState) => state.settings.qualityProfiles.items, - (qualityProfileId, qualityProfiles) => { - return qualityProfiles.find((profile) => profile.id === qualityProfileId); - } - ); -} - -export default createQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.ts b/frontend/src/Store/Selectors/createQueueItemSelector.js similarity index 52% rename from frontend/src/Store/Selectors/createQueueItemSelector.ts rename to frontend/src/Store/Selectors/createQueueItemSelector.js index 54951a724..c85d7ed82 100644 --- a/frontend/src/Store/Selectors/createQueueItemSelector.ts +++ b/frontend/src/Store/Selectors/createQueueItemSelector.js @@ -1,16 +1,21 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createQueueItemSelector() { return createSelector( - (_: AppState, { albumId }: { albumId: number }) => albumId, - (state: AppState) => state.queue.details.items, + (state, { albumId }) => albumId, + (state) => state.queue.details.items, (albumId, details) => { if (!albumId || !details) { return null; } - return details.find((item) => item.albumId === albumId); + return details.find((item) => { + if (item.album) { + return item.album.id === albumId; + } + + return false; + }); } ); } diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts deleted file mode 100644 index 432f9056d..000000000 --- a/frontend/src/Store/Selectors/createRootFoldersSelector.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createSelector } from 'reselect'; -import { RootFolderAppState } from 'App/State/SettingsAppState'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import RootFolder from 'typings/RootFolder'; -import sortByProp from 'Utilities/Array/sortByProp'; - -export default function createRootFoldersSelector() { - return createSelector( - createSortedSectionSelector( - 'settings.rootFolders', - sortByProp('name') - ), - (rootFolders: RootFolderAppState) => rootFolders - ); -} diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.ts b/frontend/src/Store/Selectors/createSortedSectionSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.ts rename to frontend/src/Store/Selectors/createSortedSectionSelector.js index abee01f75..331d890c9 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.js @@ -1,18 +1,14 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector( - section: string, - comparer: (a: T, b: T) => number -) { +function createSortedSectionSelector(section, comparer) { return createSelector( (state) => state, (state) => { const sectionState = getSectionState(state, section, true); - return { ...sectionState, - items: [...sectionState.items].sort(comparer), + items: [...sectionState.items].sort(comparer) }; } ); diff --git a/frontend/src/Store/Selectors/createSystemStatusSelector.ts b/frontend/src/Store/Selectors/createSystemStatusSelector.js similarity index 70% rename from frontend/src/Store/Selectors/createSystemStatusSelector.ts rename to frontend/src/Store/Selectors/createSystemStatusSelector.js index f5e276069..df586bbb9 100644 --- a/frontend/src/Store/Selectors/createSystemStatusSelector.ts +++ b/frontend/src/Store/Selectors/createSystemStatusSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createSystemStatusSelector() { return createSelector( - (state: AppState) => state.system.status, + (state) => state.system.status, (status) => { return status.item; } diff --git a/frontend/src/Store/Selectors/createTagDetailsSelector.ts b/frontend/src/Store/Selectors/createTagDetailsSelector.js similarity index 62% rename from frontend/src/Store/Selectors/createTagDetailsSelector.ts rename to frontend/src/Store/Selectors/createTagDetailsSelector.js index 2a271cafe..dd178944c 100644 --- a/frontend/src/Store/Selectors/createTagDetailsSelector.ts +++ b/frontend/src/Store/Selectors/createTagDetailsSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTagDetailsSelector() { return createSelector( - (_: AppState, { id }: { id: number }) => id, - (state: AppState) => state.tags.details.items, + (state, { id }) => id, + (state) => state.tags.details.items, (id, tagDetails) => { return tagDetails.find((t) => t.id === id); } diff --git a/frontend/src/Store/Selectors/createTagsSelector.ts b/frontend/src/Store/Selectors/createTagsSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createTagsSelector.ts rename to frontend/src/Store/Selectors/createTagsSelector.js index f653ff6e3..fbfd91cdb 100644 --- a/frontend/src/Store/Selectors/createTagsSelector.ts +++ b/frontend/src/Store/Selectors/createTagsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTagsSelector() { return createSelector( - (state: AppState) => state.tags.items, + (state) => state.tags.items, (tags) => { return tags; } diff --git a/frontend/src/Store/Selectors/createTrackFileSelector.ts b/frontend/src/Store/Selectors/createTrackFileSelector.js similarity index 66% rename from frontend/src/Store/Selectors/createTrackFileSelector.ts rename to frontend/src/Store/Selectors/createTrackFileSelector.js index a162df1fa..bcfc5cb0b 100644 --- a/frontend/src/Store/Selectors/createTrackFileSelector.ts +++ b/frontend/src/Store/Selectors/createTrackFileSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTrackFileSelector() { return createSelector( - (_: AppState, { trackFileId }: { trackFileId: number }) => trackFileId, - (state: AppState) => state.trackFiles, + (state, { trackFileId }) => trackFileId, + (state) => state.trackFiles, (trackFileId, trackFiles) => { if (!trackFileId) { return; diff --git a/frontend/src/Store/Selectors/createUISettingsSelector.ts b/frontend/src/Store/Selectors/createUISettingsSelector.js similarity index 69% rename from frontend/src/Store/Selectors/createUISettingsSelector.ts rename to frontend/src/Store/Selectors/createUISettingsSelector.js index ff539679b..b256d0e98 100644 --- a/frontend/src/Store/Selectors/createUISettingsSelector.ts +++ b/frontend/src/Store/Selectors/createUISettingsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createUISettingsSelector() { return createSelector( - (state: AppState) => state.settings.ui, + (state) => state.settings.ui, (ui) => { return ui.item; } diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js new file mode 100644 index 000000000..287a58593 --- /dev/null +++ b/frontend/src/Store/scrollPositions.js @@ -0,0 +1,5 @@ +const scrollPositions = { + artistIndex: 0 +}; + +export default scrollPositions; diff --git a/frontend/src/Store/scrollPositions.ts b/frontend/src/Store/scrollPositions.ts deleted file mode 100644 index 199bfa84c..000000000 --- a/frontend/src/Store/scrollPositions.ts +++ /dev/null @@ -1,5 +0,0 @@ -const scrollPositions: Record = { - artistIndex: 0, -}; - -export default scrollPositions; diff --git a/frontend/src/Store/thunks.js b/frontend/src/Store/thunks.js new file mode 100644 index 000000000..6daa843f4 --- /dev/null +++ b/frontend/src/Store/thunks.js @@ -0,0 +1,27 @@ +const thunks = {}; + +function identity(payload) { + return payload; +} + +export function createThunk(type, identityFunction = identity) { + return function(payload = {}) { + return function(dispatch, getState) { + const thunk = thunks[type]; + + if (thunk) { + return thunk(getState, identityFunction(payload), dispatch); + } + + throw Error(`Thunk handler has not been registered for ${type}`); + }; + }; +} + +export function handleThunks(handlers) { + const types = Object.keys(handlers); + + types.forEach((type) => { + thunks[type] = handlers[type]; + }); +} diff --git a/frontend/src/Store/thunks.ts b/frontend/src/Store/thunks.ts deleted file mode 100644 index fd277211e..000000000 --- a/frontend/src/Store/thunks.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Dispatch } from 'redux'; -import AppState from 'App/State/AppState'; - -type GetState = () => AppState; -type Thunk = ( - getState: GetState, - identityFn: never, - dispatch: Dispatch -) => unknown; - -const thunks: Record = {}; - -function identity(payload: T): TResult { - return payload as unknown as TResult; -} - -export function createThunk(type: string, identityFunction = identity) { - return function (payload?: T) { - return function (dispatch: Dispatch, getState: GetState) { - const thunk = thunks[type]; - - if (thunk) { - const finalPayload = payload ?? {}; - - return thunk(getState, identityFunction(finalPayload), dispatch); - } - - throw Error(`Thunk handler has not been registered for ${type}`); - }; - }; -} - -export function handleThunks(handlers: Record) { - const types = Object.keys(handlers); - - types.forEach((type) => { - thunks[type] = handlers[type]; - }); -} diff --git a/frontend/src/Styles/Themes/dark.js b/frontend/src/Styles/Themes/dark.js index 7513139a4..5e16185ca 100644 --- a/frontend/src/Styles/Themes/dark.js +++ b/frontend/src/Styles/Themes/dark.js @@ -39,7 +39,7 @@ module.exports = { themeDarkColor: '#494949', themeLightColor: '#595959', pageBackground: '#202020', - pageFooterBackground: 'rgba(0, 0, 0, .25)', + pageFooterBackgroud: 'rgba(0, 0, 0, .25)', torrentColor: '#00853d', usenetColor: '#17b1d9', diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js index 4dec39164..d93c5dd8c 100644 --- a/frontend/src/Styles/Themes/index.js +++ b/frontend/src/Styles/Themes/index.js @@ -2,7 +2,7 @@ import * as dark from './dark'; import * as light from './light'; const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const auto = defaultDark ? dark : light; +const auto = defaultDark ? { ...dark } : { ...light }; export default { auto, diff --git a/frontend/src/Styles/Themes/light.js b/frontend/src/Styles/Themes/light.js index ccf5dcea6..b7d24f92a 100644 --- a/frontend/src/Styles/Themes/light.js +++ b/frontend/src/Styles/Themes/light.js @@ -39,7 +39,7 @@ module.exports = { themeDarkColor: '#353535', themeLightColor: '#1d563d', pageBackground: '#f5f7fa', - pageFooterBackground: '#f1f1f1', + pageFooterBackgroud: '#f1f1f1', torrentColor: '#00853d', usenetColor: '#17b1d9', diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js index def48f28e..3b0077c5a 100644 --- a/frontend/src/Styles/Variables/fonts.js +++ b/frontend/src/Styles/Variables/fonts.js @@ -2,6 +2,7 @@ module.exports = { // Families defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', + passwordFamily: 'text-security-disc', // Sizes extraSmallFontSize: '11px', diff --git a/frontend/src/System/Logs/Files/LogFiles.js b/frontend/src/System/Logs/Files/LogFiles.js index 5339a8590..83736c617 100644 --- a/frontend/src/System/Logs/Files/LogFiles.js +++ b/frontend/src/System/Logs/Files/LogFiles.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; +import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; @@ -77,16 +77,15 @@ class LogFiles extends Component {
- {translate('LogFilesLocation', { - location - })} + Log files are located in: {location}
- {currentLogView === 'Log Files' ? ( -
- -
- ) : null} + { + currentLogView === 'Log Files' && +
+ The log level defaults to 'Info' and can be changed in General Settings +
+ }
{ diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js index 8af5b4717..4635fef68 100644 --- a/frontend/src/System/Status/About/About.js +++ b/frontend/src/System/Status/About/About.js @@ -22,9 +22,9 @@ class About extends Component { isNetCore, isDocker, runtimeVersion, + migrationVersion, databaseVersion, databaseType, - migrationVersion, appData, startupPath, mode, @@ -66,13 +66,13 @@ class About extends Component { } { + this.setState(getFormattedDates(this.props)); + this.setUpdateTimer(); + }, 30000); + } + + // + // Listeners + + onCancelPress = () => { + this.setState({ + isCancelConfirmModalOpen: true + }); + }; + + onAbortCancel = () => { + this.setState({ + isCancelConfirmModalOpen: false + }); + }; + + // + // Render + + render() { + const { + trigger, + commandName, + queued, + started, + ended, + status, + duration, + message, + clientUserAgent, + longDateFormat, + timeFormat, + onCancelPress + } = this.props; + + const { + queuedAt, + startedAt, + endedAt, + isCancelConfirmModalOpen + } = this.state; + + let triggerIcon = icons.QUICK; + + if (trigger === 'manual') { + triggerIcon = icons.INTERACTIVE; + } else if (trigger === 'scheduled') { + triggerIcon = icons.SCHEDULED; + } + + return ( + + + + + + + + + + + + {commandName} + + { + clientUserAgent ? + + from: {clientUserAgent} + : + null + } + + + + {queuedAt} + + + + {startedAt} + + + + {endedAt} + + + + {formatTimeSpan(duration)} + + + + { + status === 'queued' && + + } + + + + + ); + } +} + +QueuedTaskRow.propTypes = { + trigger: PropTypes.string.isRequired, + commandName: PropTypes.string.isRequired, + queued: PropTypes.string.isRequired, + started: PropTypes.string, + ended: PropTypes.string, + status: PropTypes.string.isRequired, + duration: PropTypes.string, + message: PropTypes.string, + clientUserAgent: PropTypes.string, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onCancelPress: PropTypes.func.isRequired +}; + +export default QueuedTaskRow; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx deleted file mode 100644 index 4511bcbf4..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import moment from 'moment'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { CommandBody } from 'Commands/Command'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; -import { icons, kinds } from 'Helpers/Props'; -import { cancelCommand } from 'Store/Actions/commandActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import QueuedTaskRowNameCell from './QueuedTaskRowNameCell'; -import styles from './QueuedTaskRow.css'; - -function getStatusIconProps(status: string, message: string | undefined) { - const title = titleCase(status); - - switch (status) { - case 'queued': - return { - name: icons.PENDING, - title, - }; - - case 'started': - return { - name: icons.REFRESH, - isSpinning: true, - title, - }; - - case 'completed': - return { - name: icons.CHECK, - kind: kinds.SUCCESS, - title: message === 'Completed' ? title : `${title}: ${message}`, - }; - - case 'failed': - return { - name: icons.FATAL, - kind: kinds.DANGER, - title: `${title}: ${message}`, - }; - - default: - return { - name: icons.UNKNOWN, - title, - }; - } -} - -function getFormattedDates( - queued: string, - started: string | undefined, - ended: string | undefined, - showRelativeDates: boolean, - shortDateFormat: string -) { - if (showRelativeDates) { - return { - queuedAt: moment(queued).fromNow(), - startedAt: started ? moment(started).fromNow() : '-', - endedAt: ended ? moment(ended).fromNow() : '-', - }; - } - - return { - queuedAt: formatDate(queued, shortDateFormat), - startedAt: started ? formatDate(started, shortDateFormat) : '-', - endedAt: ended ? formatDate(ended, shortDateFormat) : '-', - }; -} - -interface QueuedTimes { - queuedAt: string; - startedAt: string; - endedAt: string; -} - -export interface QueuedTaskRowProps { - id: number; - trigger: string; - commandName: string; - queued: string; - started?: string; - ended?: string; - status: string; - duration?: string; - message?: string; - body: CommandBody; - clientUserAgent?: string; -} - -export default function QueuedTaskRow(props: QueuedTaskRowProps) { - const { - id, - trigger, - commandName, - queued, - started, - ended, - status, - duration, - message, - body, - clientUserAgent, - } = props; - - const dispatch = useDispatch(); - const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } = - useSelector(createUISettingsSelector()); - - const updateTimeTimeoutId = useRef | null>( - null - ); - const [times, setTimes] = useState( - getFormattedDates( - queued, - started, - ended, - showRelativeDates, - shortDateFormat - ) - ); - - const [ - isCancelConfirmModalOpen, - openCancelConfirmModal, - closeCancelConfirmModal, - ] = useModalOpenState(false); - - const handleCancelPress = useCallback(() => { - dispatch(cancelCommand({ id })); - }, [id, dispatch]); - - useEffect(() => { - updateTimeTimeoutId.current = setTimeout(() => { - setTimes( - getFormattedDates( - queued, - started, - ended, - showRelativeDates, - shortDateFormat - ) - ); - }, 30000); - - return () => { - if (updateTimeTimeoutId.current) { - clearTimeout(updateTimeTimeoutId.current); - } - }; - }, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]); - - const { queuedAt, startedAt, endedAt } = times; - - let triggerIcon = icons.QUICK; - - if (trigger === 'manual') { - triggerIcon = icons.INTERACTIVE; - } else if (trigger === 'scheduled') { - triggerIcon = icons.SCHEDULED; - } - - return ( - - - - - - - - - - - - - {queuedAt} - - - - {startedAt} - - - - {endedAt} - - - - {formatTimeSpan(duration)} - - - - {status === 'queued' && ( - - )} - - - - - ); -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js new file mode 100644 index 000000000..f55ab985a --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { cancelCommand } from 'Store/Actions/commandActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import QueuedTaskRow from './QueuedTaskRow'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return { + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onCancelPress() { + dispatch(cancelCommand({ + id: props.id + })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css deleted file mode 100644 index 41acb33f8..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css +++ /dev/null @@ -1,8 +0,0 @@ -.commandName { - display: inline-block; - min-width: 220px; -} - -.userAgent { - color: #b0b0b0; -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts deleted file mode 100644 index fc9081492..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'commandName': string; - 'userAgent': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx deleted file mode 100644 index 41a307d5f..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { CommandBody } from 'Commands/Command'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import createMultiArtistsSelector from 'Store/Selectors/createMultiArtistsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import styles from './QueuedTaskRowNameCell.css'; - -function formatTitles(titles: string[]) { - if (!titles) { - return null; - } - - if (titles.length > 11) { - return ( - - {titles.slice(0, 10).join(', ')}, {titles.length - 10} more - - ); - } - - return {titles.join(', ')}; -} - -export interface QueuedTaskRowNameCellProps { - commandName: string; - body: CommandBody; - clientUserAgent?: string; -} - -export default function QueuedTaskRowNameCell( - props: QueuedTaskRowNameCellProps -) { - const { commandName, body, clientUserAgent } = props; - const movieIds = [...(body.artistIds ?? [])]; - - if (body.artistId) { - movieIds.push(body.artistId); - } - - const artists = useSelector(createMultiArtistsSelector(movieIds)); - const sortedArtists = artists.sort(sortByProp('sortName')); - - return ( - - - {commandName} - {sortedArtists.length ? ( - - {formatTitles(sortedArtists.map((a) => a.artistName))} - ) : null} - - - {clientUserAgent ? ( - - {translate('From')}: {clientUserAgent} - - ) : null} - - ); -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js new file mode 100644 index 000000000..dac38f1d4 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasks.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRowConnector from './QueuedTaskRowConnector'; + +const columns = [ + { + name: 'trigger', + label: '', + isVisible: true + }, + { + name: 'commandName', + label: () => translate('Name'), + isVisible: true + }, + { + name: 'queued', + label: () => translate('Queued'), + isVisible: true + }, + { + name: 'started', + label: () => translate('Started'), + isVisible: true + }, + { + name: 'ended', + label: () => translate('Ended'), + isVisible: true + }, + { + name: 'duration', + label: () => translate('Duration'), + isVisible: true + }, + { + name: 'actions', + isVisible: true + } +]; + +function QueuedTasks(props) { + const { + isFetching, + isPopulated, + items + } = props; + + return ( +
+ { + isFetching && !isPopulated && + + } + + { + isPopulated && +
+ + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } +
+ ); +} + +QueuedTasks.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default QueuedTasks; diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx deleted file mode 100644 index e79deed7c..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { fetchCommands } from 'Store/Actions/commandActions'; -import translate from 'Utilities/String/translate'; -import QueuedTaskRow from './QueuedTaskRow'; - -const columns = [ - { - name: 'trigger', - label: '', - isVisible: true, - }, - { - name: 'commandName', - label: () => translate('Name'), - isVisible: true, - }, - { - name: 'queued', - label: () => translate('Queued'), - isVisible: true, - }, - { - name: 'started', - label: () => translate('Started'), - isVisible: true, - }, - { - name: 'ended', - label: () => translate('Ended'), - isVisible: true, - }, - { - name: 'duration', - label: () => translate('Duration'), - isVisible: true, - }, - { - name: 'actions', - isVisible: true, - }, -]; - -export default function QueuedTasks() { - const dispatch = useDispatch(); - const { isFetching, isPopulated, items } = useSelector( - (state: AppState) => state.commands - ); - - useEffect(() => { - dispatch(fetchCommands()); - }, [dispatch]); - - return ( -
- {isFetching && !isPopulated && } - - {isPopulated && ( - - - {items.map((item) => { - return ; - })} - -
- )} -
- ); -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js new file mode 100644 index 000000000..5fa4d9ead --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchCommands } from 'Store/Actions/commandActions'; +import QueuedTasks from './QueuedTasks'; + +function createMapStateToProps() { + return createSelector( + (state) => state.commands, + (commands) => { + return commands; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchCommands: fetchCommands +}; + +class QueuedTasksConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchCommands(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QueuedTasksConnector.propTypes = { + dispatchFetchCommands: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js index 03a3b6ce4..032dbede8 100644 --- a/frontend/src/System/Tasks/Tasks.js +++ b/frontend/src/System/Tasks/Tasks.js @@ -2,7 +2,7 @@ import React from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; -import QueuedTasks from './Queued/QueuedTasks'; +import QueuedTasksConnector from './Queued/QueuedTasksConnector'; import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; function Tasks() { @@ -10,7 +10,7 @@ function Tasks() { - + ); diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js new file mode 100644 index 000000000..3588069a0 --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import styles from './UpdateChanges.css'; + +class UpdateChanges extends Component { + + // + // Render + + render() { + const { + title, + changes + } = this.props; + + if (changes.length === 0) { + return null; + } + + return ( +
+
{title}
+
    + { + changes.map((change, index) => { + return ( +
  • + +
  • + ); + }) + } +
+
+ ); + } + +} + +UpdateChanges.propTypes = { + title: PropTypes.string.isRequired, + changes: PropTypes.arrayOf(PropTypes.string) +}; + +export default UpdateChanges; diff --git a/frontend/src/System/Updates/UpdateChanges.tsx b/frontend/src/System/Updates/UpdateChanges.tsx deleted file mode 100644 index 3e5ba1c9b..000000000 --- a/frontend/src/System/Updates/UpdateChanges.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import styles from './UpdateChanges.css'; - -interface UpdateChangesProps { - title: string; - changes: string[]; -} - -function UpdateChanges(props: UpdateChangesProps) { - const { title, changes } = props; - - if (changes.length === 0) { - return null; - } - - const uniqueChanges = [...new Set(changes)]; - - return ( -
-
{title}
-
    - {uniqueChanges.map((change, index) => { - const checkChange = change.replace( - /#\d{4,5}\b/g, - (match) => - `[${match}](https://github.com/Lidarr/Lidarr/issues/${match.substring( - 1 - )})` - ); - - return ( -
  • - -
  • - ); - })} -
-
- ); -} - -export default UpdateChanges; diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js new file mode 100644 index 000000000..528441cbe --- /dev/null +++ b/frontend/src/System/Updates/Updates.js @@ -0,0 +1,249 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons, kinds } from 'Helpers/Props'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import translate from 'Utilities/String/translate'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +class Updates extends Component { + + // + // Render + + render() { + const { + currentVersion, + isFetching, + isPopulated, + updatesError, + generalSettingsError, + items, + isInstallingUpdate, + updateMechanism, + updateMechanismMessage, + shortDateFormat, + longDateFormat, + timeFormat, + onInstallLatestPress + } = this.props; + + const hasError = !!(updatesError || generalSettingsError); + const hasUpdates = isPopulated && !hasError && items.length > 0; + const noUpdates = isPopulated && !hasError && !items.length; + const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + const externalUpdaterPrefix = 'Unable to update Lidarr directly,'; + const externalUpdaterMessages = { + external: 'Lidarr is configured to use an external update mechanism', + apt: 'use apt to install the update', + docker: 'update the docker container to receive the update' + }; + + return ( + + + { + !isPopulated && !hasError && + + } + + { + noUpdates && + + {translate('NoUpdatesAreAvailable')} + + } + + { + hasUpdateToInstall && +
+ { + updateMechanism === 'builtIn' || updateMechanism === 'script' ? + + Install Latest + : + + + + +
+ {externalUpdaterPrefix} +
+
+ } + + { + isFetching && + + } +
+ } + + { + noUpdateToInstall && +
+ +
+ The latest version of Lidarr is already installed +
+ + { + isFetching && + + } +
+ } + + { + hasUpdates && +
+ { + items.map((update) => { + const hasChanges = !!update.changes; + + return ( +
+
+
{update.version}
+
+
+ {formatDate(update.releaseDate, shortDateFormat)} +
+ + { + update.branch === 'master' ? + null : + + } + + { + update.version === currentVersion ? + : + null + } + + { + update.version !== currentVersion && update.installedOn ? + : + null + } +
+ + { + !hasChanges && +
+ {translate('MaintenanceRelease')} +
+ } + + { + hasChanges && +
+ + + +
+ } +
+ ); + }) + } +
+ } + + { + !!updatesError && +
+ Failed to fetch updates +
+ } + + { + !!generalSettingsError && +
+ Failed to update settings +
+ } +
+
+ ); + } + +} + +Updates.propTypes = { + currentVersion: PropTypes.string.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + updatesError: PropTypes.object, + generalSettingsError: PropTypes.object, + items: PropTypes.array.isRequired, + isInstallingUpdate: PropTypes.bool.isRequired, + updateMechanism: PropTypes.string, + updateMechanismMessage: PropTypes.string, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onInstallLatestPress: PropTypes.func.isRequired +}; + +export default Updates; diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx deleted file mode 100644 index 300ab1f99..000000000 --- a/frontend/src/System/Updates/Updates.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import * as commandNames from 'Commands/commandNames'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons, kinds } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import { UpdateMechanism } from 'typings/Settings/General'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import translate from 'Utilities/String/translate'; -import UpdateChanges from './UpdateChanges'; -import styles from './Updates.css'; - -const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; - -function createUpdatesSelector() { - return createSelector( - (state: AppState) => state.system.updates, - (state: AppState) => state.settings.general, - (updates, generalSettings) => { - const { error: updatesError, items } = updates; - - const isFetching = updates.isFetching || generalSettings.isFetching; - const isPopulated = updates.isPopulated && generalSettings.isPopulated; - - return { - isFetching, - isPopulated, - updatesError, - generalSettingsError: generalSettings.error, - items, - updateMechanism: generalSettings.item.updateMechanism, - }; - } - ); -} - -function Updates() { - const currentVersion = useSelector((state: AppState) => state.app.version); - const { packageUpdateMechanismMessage } = useSelector( - createSystemStatusSelector() - ); - const { shortDateFormat, longDateFormat, timeFormat } = useSelector( - createUISettingsSelector() - ); - const isInstallingUpdate = useSelector( - createCommandExecutingSelector(commandNames.APPLICATION_UPDATE) - ); - - const { - isFetching, - isPopulated, - updatesError, - generalSettingsError, - items, - updateMechanism, - } = useSelector(createUpdatesSelector()); - - const dispatch = useDispatch(); - const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); - const hasError = !!(updatesError || generalSettingsError); - const hasUpdates = isPopulated && !hasError && items.length > 0; - const noUpdates = isPopulated && !hasError && !items.length; - - const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); - const externalUpdaterMessages: Partial> = { - external: translate('ExternalUpdater'), - apt: translate('AptUpdater'), - docker: translate('DockerUpdater'), - }; - - const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => { - const majorVersion = parseInt( - currentVersion.match(VERSION_REGEX)?.[0] ?? '0' - ); - - const latestVersion = items[0]?.version; - const latestMajorVersion = parseInt( - latestVersion?.match(VERSION_REGEX)?.[0] ?? '0' - ); - - return { - isMajorUpdate: latestMajorVersion > majorVersion, - hasUpdateToInstall: items.some( - (update) => update.installable && update.latest - ), - }; - }, [currentVersion, items]); - - const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; - - const handleInstallLatestPress = useCallback(() => { - if (isMajorUpdate) { - setIsMajorUpdateModalOpen(true); - } else { - dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE })); - } - }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]); - - const handleInstallLatestMajorVersionPress = useCallback(() => { - setIsMajorUpdateModalOpen(false); - - dispatch( - executeCommand({ - name: commandNames.APPLICATION_UPDATE, - installMajorUpdate: true, - }) - ); - }, [setIsMajorUpdateModalOpen, dispatch]); - - const handleCancelMajorVersionPress = useCallback(() => { - setIsMajorUpdateModalOpen(false); - }, [setIsMajorUpdateModalOpen]); - - useEffect(() => { - dispatch(fetchUpdates()); - dispatch(fetchGeneralSettings()); - }, [dispatch]); - - return ( - - - {isPopulated || hasError ? null : } - - {noUpdates ? ( - {translate('NoUpdatesAreAvailable')} - ) : null} - - {hasUpdateToInstall ? ( -
- {updateMechanism === 'builtIn' || updateMechanism === 'script' ? ( - - {translate('InstallLatest')} - - ) : ( - <> - - -
- {externalUpdaterPrefix}{' '} - -
- - )} - - {isFetching ? ( - - ) : null} -
- ) : null} - - {noUpdateToInstall && ( -
- -
{translate('OnLatestVersion')}
- - {isFetching && ( - - )} -
- )} - - {hasUpdates && ( -
- {items.map((update) => { - return ( -
-
-
{update.version}
-
-
- {formatDate(update.releaseDate, shortDateFormat)} -
- - {update.branch === 'master' ? null : ( - - )} - - {update.version === currentVersion ? ( - - ) : null} - - {update.version !== currentVersion && update.installedOn ? ( - - ) : null} -
- - {update.changes ? ( -
- - - -
- ) : ( -
{translate('MaintenanceRelease')}
- )} -
- ); - })} -
- )} - - {updatesError ? ( - - {translate('FailedToFetchUpdates')} - - ) : null} - - {generalSettingsError ? ( - - {translate('FailedToFetchSettings')} - - ) : null} - - -
{translate('InstallMajorVersionUpdateMessage')}
-
- -
-
- } - confirmLabel={translate('Install')} - onConfirm={handleInstallLatestMajorVersionPress} - onCancel={handleCancelMajorVersionPress} - /> - - - ); -} - -export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js new file mode 100644 index 000000000..77d75dbda --- /dev/null +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -0,0 +1,98 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as commandNames from 'Commands/commandNames'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import Updates from './Updates'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.version, + createSystemStatusSelector(), + (state) => state.system.updates, + (state) => state.settings.general, + createUISettingsSelector(), + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), + ( + currentVersion, + status, + updates, + generalSettings, + uiSettings, + isInstallingUpdate + ) => { + const { + error: updatesError, + items + } = updates; + + const isFetching = updates.isFetching || generalSettings.isFetching; + const isPopulated = updates.isPopulated && generalSettings.isPopulated; + + return { + currentVersion, + isFetching, + isPopulated, + updatesError, + generalSettingsError: generalSettings.error, + items, + isInstallingUpdate, + updateMechanism: generalSettings.item.updateMechanism, + updateMechanismMessage: status.packageUpdateMechanismMessage, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchUpdates: fetchUpdates, + dispatchFetchGeneralSettings: fetchGeneralSettings, + dispatchExecuteCommand: executeCommand +}; + +class UpdatesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchUpdates(); + this.props.dispatchFetchGeneralSettings(); + } + + // + // Listeners + + onInstallLatestPress = () => { + this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +UpdatesConnector.propTypes = { + dispatchFetchUpdates: PropTypes.func.isRequired, + dispatchFetchGeneralSettings: PropTypes.func.isRequired, + dispatchExecuteCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/Track/Track.ts b/frontend/src/Track/Track.ts deleted file mode 100644 index 7c080290a..000000000 --- a/frontend/src/Track/Track.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -interface Track extends ModelBase { - artistId: number; - foreignTrackId: string; - foreignRecordingId: string; - trackFileId: number; - albumId: number; - explicit: boolean; - absoluteTrackNumber: number; - trackNumber: string; - title: string; - duration: number; - trackFile?: object; - mediumNumber: number; - hasFile: boolean; -} - -export default Track; diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js index 0e387f39f..aa59e866f 100644 --- a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js +++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js @@ -146,7 +146,7 @@ class TrackFileEditorModalContent extends Component { }); return acc; - }, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]); + }, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]); const hasSelectedFiles = this.getSelectedIds().length > 0; diff --git a/frontend/src/TrackFile/TrackFile.ts b/frontend/src/TrackFile/TrackFile.ts deleted file mode 100644 index ef4dc65f3..000000000 --- a/frontend/src/TrackFile/TrackFile.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ModelBase from 'App/ModelBase'; -import { QualityModel } from 'Quality/Quality'; -import CustomFormat from 'typings/CustomFormat'; -import MediaInfo from 'typings/MediaInfo'; - -export interface TrackFile extends ModelBase { - artistId: number; - albumId: number; - path: string; - size: number; - dateAdded: string; - sceneName: string; - releaseGroup: string; - quality: QualityModel; - customFormats: CustomFormat[]; - indexerFlags: number; - mediaInfo: MediaInfo; - qualityCutoffNotMet: boolean; -} diff --git a/frontend/src/UnmappedFiles/UnmappedFilesTable.js b/frontend/src/UnmappedFiles/UnmappedFilesTable.js index a326f91e0..817826cc1 100644 --- a/frontend/src/UnmappedFiles/UnmappedFilesTable.js +++ b/frontend/src/UnmappedFiles/UnmappedFilesTable.js @@ -27,9 +27,8 @@ class UnmappedFilesTable extends Component { constructor(props, context) { super(props, context); - this.scrollerRef = React.createRef(); - this.state = { + scroller: null, allSelected: false, allUnselected: false, lastToggled: null, @@ -66,6 +65,13 @@ class UnmappedFilesTable extends Component { } } + // + // Control + + setScrollerRef = (ref) => { + this.setState({ scroller: ref }); + }; + getSelectedIds = () => { if (this.state.allUnselected) { return []; @@ -178,6 +184,7 @@ class UnmappedFilesTable extends Component { } = this.props; const { + scroller, allSelected, allUnselected, selectedState @@ -220,7 +227,9 @@ class UnmappedFilesTable extends Component { - + { isFetching && !isPopulated && @@ -234,14 +243,11 @@ class UnmappedFilesTable extends Component { } { - isPopulated && - !error && - !!items.length && - this.scrollerRef.current ? + isPopulated && !error && !!items.length && scroller && : - null + /> } diff --git a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js index cec7fb09a..d27cfd604 100644 --- a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js +++ b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js @@ -1,5 +1,7 @@ +import _ from 'lodash'; + export default function getIndexOfFirstCharacter(items, character) { - return items.findIndex((item) => { + return _.findIndex(items, (item) => { const firstCharacter = item.sortName.charAt(0); if (character === '#') { diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js new file mode 100644 index 000000000..1956d3bac --- /dev/null +++ b/frontend/src/Utilities/Array/sortByName.js @@ -0,0 +1,5 @@ +function sortByName(a, b) { + return a.name.localeCompare(b.name); +} + +export default sortByName; diff --git a/frontend/src/Utilities/Array/sortByProp.ts b/frontend/src/Utilities/Array/sortByProp.ts deleted file mode 100644 index 8fbde08c9..000000000 --- a/frontend/src/Utilities/Array/sortByProp.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { StringKey } from 'typings/Helpers/KeysMatching'; - -export function sortByProp< - // eslint-disable-next-line no-use-before-define - T extends Record, - K extends StringKey ->(sortKey: K) { - return (a: T, b: T) => { - return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true }); - }; -} - -export default sortByProp; diff --git a/frontend/src/Utilities/Artist/getProgressBarKind.ts b/frontend/src/Utilities/Artist/getProgressBarKind.js similarity index 58% rename from frontend/src/Utilities/Artist/getProgressBarKind.ts rename to frontend/src/Utilities/Artist/getProgressBarKind.js index f45387024..eb3b2dd6e 100644 --- a/frontend/src/Utilities/Artist/getProgressBarKind.ts +++ b/frontend/src/Utilities/Artist/getProgressBarKind.js @@ -1,15 +1,6 @@ import { kinds } from 'Helpers/Props'; -function getProgressBarKind( - status: string, - monitored: boolean, - progress: number, - isDownloading: boolean -) { - if (isDownloading) { - return kinds.PURPLE; - } - +function getProgressBarKind(status, monitored, progress) { if (progress === 100) { return status === 'ended' ? kinds.SUCCESS : kinds.PRIMARY; } diff --git a/frontend/src/Utilities/Artist/monitorNewItemsOptions.js b/frontend/src/Utilities/Artist/monitorNewItemsOptions.js index f45095b6e..2f352be3a 100644 --- a/frontend/src/Utilities/Artist/monitorNewItemsOptions.js +++ b/frontend/src/Utilities/Artist/monitorNewItemsOptions.js @@ -4,19 +4,19 @@ const monitorNewItemsOptions = [ { key: 'all', get value() { - return translate('MonitorAllAlbums'); + return translate('AllAlbums'); } }, { key: 'none', get value() { - return translate('MonitorNoNewAlbums'); + return translate('None'); } }, { key: 'new', get value() { - return translate('MonitorNewAlbums'); + return translate('New'); } } ]; diff --git a/frontend/src/Utilities/Artist/monitorOptions.js b/frontend/src/Utilities/Artist/monitorOptions.js index a06a79a96..b5e942ae6 100644 --- a/frontend/src/Utilities/Artist/monitorOptions.js +++ b/frontend/src/Utilities/Artist/monitorOptions.js @@ -1,48 +1,11 @@ -import translate from 'Utilities/String/translate'; - const monitorOptions = [ - { - key: 'all', - get value() { - return translate('MonitorAllAlbums'); - } - }, - { - key: 'future', - get value() { - return translate('MonitorFutureAlbums'); - } - }, - { - key: 'missing', - get value() { - return translate('MonitorMissingAlbums'); - } - }, - { - key: 'existing', - get value() { - return translate('MonitorExistingAlbums'); - } - }, - { - key: 'first', - get value() { - return translate('MonitorFirstAlbum'); - } - }, - { - key: 'latest', - get value() { - return translate('MonitorLastestAlbum'); - } - }, - { - key: 'none', - get value() { - return translate('MonitorNoAlbums'); - } - } + { key: 'all', value: 'All Albums' }, + { key: 'future', value: 'Future Albums' }, + { key: 'missing', value: 'Missing Albums' }, + { key: 'existing', value: 'Existing Albums' }, + { key: 'first', value: 'Only First Album' }, + { key: 'latest', value: 'Only Latest Album' }, + { key: 'none', value: 'None' } ]; export default monitorOptions; diff --git a/frontend/src/Utilities/Date/formatDateTime.js b/frontend/src/Utilities/Date/formatDateTime.js index fb50230e1..f36f4f3e0 100644 --- a/frontend/src/Utilities/Date/formatDateTime.js +++ b/frontend/src/Utilities/Date/formatDateTime.js @@ -1,5 +1,4 @@ import moment from 'moment'; -import translate from 'Utilities/String/translate'; import formatTime from './formatTime'; import isToday from './isToday'; import isTomorrow from './isTomorrow'; @@ -11,15 +10,15 @@ function getRelativeDay(date, includeRelativeDate) { } if (isYesterday(date)) { - return translate('Yesterday'); + return 'Yesterday, '; } if (isToday(date)) { - return translate('Today'); + return 'Today, '; } if (isTomorrow(date)) { - return translate('Tomorrow'); + return 'Tomorrow, '; } return ''; @@ -34,10 +33,7 @@ function formatDateTime(date, dateFormat, timeFormat, { includeSeconds = false, const formattedDate = moment(date).format(dateFormat); const formattedTime = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); - if (relativeDay) { - return translate('FormatDateTimeRelative', { relativeDay, formattedDate, formattedTime }); - } - return translate('FormatDateTime', { formattedDate, formattedTime }); + return `${relativeDay}${formattedDate} ${formattedTime}`; } export default formatDateTime; diff --git a/frontend/src/Utilities/Date/formatShortTimeSpan.js b/frontend/src/Utilities/Date/formatShortTimeSpan.js index 148dc2627..c14251e68 100644 --- a/frontend/src/Utilities/Date/formatShortTimeSpan.js +++ b/frontend/src/Utilities/Date/formatShortTimeSpan.js @@ -1,5 +1,4 @@ import moment from 'moment'; -import translate from 'Utilities/String/translate'; function formatShortTimeSpan(timeSpan) { if (!timeSpan) { @@ -13,14 +12,14 @@ function formatShortTimeSpan(timeSpan) { const seconds = Math.floor(duration.asSeconds()); if (hours > 0) { - return translate('FormatShortTimeSpanHours', { hours }); + return `${hours} hour(s)`; } if (minutes > 0) { - return translate('FormatShortTimeSpanMinutes', { minutes }); + return `${minutes} minute(s)`; } - return translate('FormatShortTimeSpanSeconds', { seconds }); + return `${seconds} second(s)`; } export default formatShortTimeSpan; diff --git a/frontend/src/Utilities/Date/formatTimeSpan.js b/frontend/src/Utilities/Date/formatTimeSpan.js index 2422e19d5..1ebe6b9e3 100644 --- a/frontend/src/Utilities/Date/formatTimeSpan.js +++ b/frontend/src/Utilities/Date/formatTimeSpan.js @@ -1,6 +1,5 @@ import moment from 'moment'; import padNumber from 'Utilities/Number/padNumber'; -import translate from 'Utilities/String/translate'; function formatTimeSpan(timeSpan) { if (!timeSpan) { @@ -17,7 +16,7 @@ function formatTimeSpan(timeSpan) { const time = `${hours}:${minutes}:${seconds}`; if (days > 0) { - return translate('FormatTimeSpanDays', { days, time }); + return `${days}d ${time}`; } return time; diff --git a/frontend/src/Utilities/Date/getRelativeDate.ts b/frontend/src/Utilities/Date/getRelativeDate.js similarity index 54% rename from frontend/src/Utilities/Date/getRelativeDate.ts rename to frontend/src/Utilities/Date/getRelativeDate.js index 178d14fb7..0a60135ce 100644 --- a/frontend/src/Utilities/Date/getRelativeDate.ts +++ b/frontend/src/Utilities/Date/getRelativeDate.js @@ -4,35 +4,16 @@ import isInNextWeek from 'Utilities/Date/isInNextWeek'; import isToday from 'Utilities/Date/isToday'; import isTomorrow from 'Utilities/Date/isTomorrow'; import isYesterday from 'Utilities/Date/isYesterday'; -import translate from 'Utilities/String/translate'; -interface GetRelativeDateOptions { - timeFormat?: string; - includeSeconds?: boolean; - timeForToday?: boolean; -} - -function getRelativeDate( - date: string | undefined, - shortDateFormat: string, - showRelativeDates: boolean, - { - timeFormat, - includeSeconds = false, - timeForToday = false, - }: GetRelativeDateOptions = {} -) { +function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) { if (!date) { - return ''; + return null; } const isTodayDate = isToday(date); if (isTodayDate && timeForToday && timeFormat) { - return formatTime(date, timeFormat, { - includeMinuteZero: true, - includeSeconds, - }); + return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); } if (!showRelativeDates) { @@ -40,15 +21,15 @@ function getRelativeDate( } if (isYesterday(date)) { - return translate('Yesterday'); + return 'Yesterday'; } if (isTodayDate) { - return translate('Today'); + return 'Today'; } if (isTomorrow(date)) { - return translate('Tomorrow'); + return 'Tomorrow'; } if (isInNextWeek(date)) { diff --git a/frontend/src/Utilities/Number/formatAge.js b/frontend/src/Utilities/Number/formatAge.js index a8f0e9f65..b8a4aacc5 100644 --- a/frontend/src/Utilities/Number/formatAge.js +++ b/frontend/src/Utilities/Number/formatAge.js @@ -1,5 +1,3 @@ -import translate from 'Utilities/String/translate'; - function formatAge(age, ageHours, ageMinutes) { age = Math.round(age); ageHours = parseFloat(ageHours); @@ -7,13 +5,13 @@ function formatAge(age, ageHours, ageMinutes) { if (age < 2 && ageHours) { if (ageHours < 2 && !!ageMinutes) { - return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? translate('FormatAgeMinute') : translate('FormatAgeMinutes')}`; + return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? 'minute' : 'minutes'}`; } - return `${ageHours.toFixed(1)} ${ageHours === 1 ? translate('FormatAgeHour') : translate('FormatAgeHours')}`; + return `${ageHours.toFixed(1)} ${ageHours === 1 ? 'hour' : 'hours'}`; } - return `${age} ${age === 1 ? translate('FormatAgeDay') : translate('FormatAgeDays')}`; + return `${age} ${age === 1 ? 'day' : 'days'}`; } export default formatAge; diff --git a/frontend/src/Utilities/String/firstCharToUpper.js b/frontend/src/Utilities/String/firstCharToUpper.js deleted file mode 100644 index 1ce64831c..000000000 --- a/frontend/src/Utilities/String/firstCharToUpper.js +++ /dev/null @@ -1,9 +0,0 @@ -function firstCharToUpper(input) { - if (!input) { - return ''; - } - - return [].map.call(input, (char, i) => (i ? char : char.toUpperCase())).join(''); -} - -export default firstCharToUpper; diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts index 5571ef6b0..0ccfa839b 100644 --- a/frontend/src/Utilities/String/translate.ts +++ b/frontend/src/Utilities/String/translate.ts @@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise { translations = data.strings; resolve(true); - } catch { + } catch (error) { resolve(false); } }); @@ -25,24 +25,20 @@ export async function fetchTranslations(): Promise { export default function translate( key: string, - tokens: Record = {} + tokens?: Record ) { - const { isProduction = true } = window.Lidarr; - - if (!isProduction && !(key in translations)) { - console.warn(`Missing translation for key: ${key}`); - } - const translation = translations[key] || key; - tokens.appName = 'Lidarr'; + if (tokens) { + // Fallback to the old behaviour for translations not yet updated to use named tokens + Object.values(tokens).forEach((value, index) => { + tokens[index] = value; + }); - // Fallback to the old behaviour for translations not yet updated to use named tokens - Object.values(tokens).forEach((value, index) => { - tokens[index] = value; - }); + return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => + String(tokens[tokenMatch] ?? match) + ); + } - return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => - String(tokens[tokenMatch] ?? match) - ); + return translation; } diff --git a/frontend/src/Utilities/Table/getSelectedIds.js b/frontend/src/Utilities/Table/getSelectedIds.js new file mode 100644 index 000000000..705f13a5d --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +function getSelectedIds(selectedState, { parseIds = true } = {}) { + return _.reduce(selectedState, (result, value, id) => { + if (value) { + const parsedId = parseIds ? parseInt(id) : id; + + result.push(parsedId); + } + + return result; + }, []); +} + +export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/getSelectedIds.ts b/frontend/src/Utilities/Table/getSelectedIds.ts deleted file mode 100644 index b84db6245..000000000 --- a/frontend/src/Utilities/Table/getSelectedIds.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { reduce } from 'lodash'; -import { SelectedState } from 'Helpers/Hooks/useSelectState'; - -function getSelectedIds(selectedState: SelectedState): number[] { - return reduce( - selectedState, - (result: number[], value, id) => { - if (value) { - result.push(parseInt(id)); - } - - return result; - }, - [] - ); -} - -export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/toggleSelected.js b/frontend/src/Utilities/Table/toggleSelected.js index ec8870b0b..dbc0d6223 100644 --- a/frontend/src/Utilities/Table/toggleSelected.js +++ b/frontend/src/Utilities/Table/toggleSelected.js @@ -1,29 +1,29 @@ import areAllSelected from './areAllSelected'; import getToggledRange from './getToggledRange'; -function toggleSelected(selectedState, items, id, selected, shiftKey) { - const lastToggled = selectedState.lastToggled; - const nextSelectedState = { - ...selectedState.selectedState, +function toggleSelected(state, items, id, selected, shiftKey) { + const lastToggled = state.lastToggled; + const selectedState = { + ...state.selectedState, [id]: selected }; if (selected == null) { - delete nextSelectedState[id]; + delete selectedState[id]; } if (shiftKey && lastToggled) { const { lower, upper } = getToggledRange(items, id, lastToggled); for (let i = lower; i < upper; i++) { - nextSelectedState[items[i].id] = selected; + selectedState[items[i].id] = selected; } } return { - ...areAllSelected(nextSelectedState), + ...areAllSelected(selectedState), lastToggled: id, - selectedState: nextSelectedState + selectedState }; } diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index 6710118b1..7e3b971ad 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -12,7 +12,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TablePager from 'Components/Table/TablePager'; import { align, icons, kinds } from 'Helpers/Props'; import getFilterValue from 'Utilities/Filter/getFilterValue'; @@ -174,16 +173,6 @@ class CutoffUnmet extends Component { - - - -
- {translate('SearchForAllCutoffUnmetAlbumsConfirmationCount', { totalRecords })} + {translate('MassAlbumsCutoffUnmetWarning', [totalRecords])}
- {translate('MassSearchCancelWarning')} + {translate('ThisCannotBeCancelled')}
} diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js index 1dd9870d1..9ee605335 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -53,7 +53,7 @@ class CutoffUnmetConnector extends Component { gotoCutoffUnmetFirstPage } = this.props; - registerPagePopulator(this.repopulate, ['trackFileUpdated', 'trackFileDeleted']); + registerPagePopulator(this.repopulate, ['trackFileUpdated']); if (useCurrentPage) { fetchCutoffUnmet(); @@ -131,15 +131,13 @@ class CutoffUnmetConnector extends Component { onSearchSelectedPress = (selected) => { this.props.executeCommand({ name: commandNames.ALBUM_SEARCH, - albumIds: selected, - commandFinished: this.repopulate + albumIds: selected }); }; onSearchAllCutoffUnmetPress = () => { this.props.executeCommand({ - name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH, - commandFinished: this.repopulate + name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH }); }; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 452e2947a..785b9b1c1 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -20,7 +20,6 @@ function CutoffUnmetRow(props) { foreignAlbumId, albumType, title, - lastSearchTime, disambiguation, isSelected, columns, @@ -90,15 +89,6 @@ function CutoffUnmetRow(props) { ); } - if (name === 'albums.lastSearchTime') { - return ( - - ); - } - if (name === 'status') { return ( - - - -
- {translate('SearchForAllMissingAlbumsConfirmationCount', { totalRecords })} + {translate('MassAlbumsSearchWarning', [totalRecords])}
- {translate('MassSearchCancelWarning')} + {translate('ThisCannotBeCancelled')}
} diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js index 008f1a149..ed4972022 100644 --- a/frontend/src/Wanted/Missing/MissingConnector.js +++ b/frontend/src/Wanted/Missing/MissingConnector.js @@ -50,7 +50,7 @@ class MissingConnector extends Component { gotoMissingFirstPage } = this.props; - registerPagePopulator(this.repopulate, ['trackFileUpdated', 'trackFileDeleted']); + registerPagePopulator(this.repopulate, ['trackFileUpdated']); if (useCurrentPage) { fetchMissing(); @@ -121,15 +121,13 @@ class MissingConnector extends Component { onSearchSelectedPress = (selected) => { this.props.executeCommand({ name: commandNames.ALBUM_SEARCH, - albumIds: selected, - commandFinished: this.repopulate + albumIds: selected }); }; onSearchAllMissingPress = () => { this.props.executeCommand({ - name: commandNames.MISSING_ALBUM_SEARCH, - commandFinished: this.repopulate + name: commandNames.MISSING_ALBUM_SEARCH }); }; diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js index 6c0b5a0c6..0eb1a0452 100644 --- a/frontend/src/Wanted/Missing/MissingRow.js +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -17,7 +17,6 @@ function MissingRow(props) { albumType, foreignAlbumId, title, - lastSearchTime, disambiguation, isSelected, columns, @@ -87,15 +86,6 @@ function MissingRow(props) { ); } - if (name === 'albums.lastSearchTime') { - return ( - - ); - } - if (name === 'actions') { return ( ); + render( + , + document.getElementById('root') + ); } diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index a893149d5..b99a39a0d 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -3,16 +3,13 @@ + + - - - - - @@ -33,7 +30,7 @@ sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png" /> - + diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 37e780919..36aed4c4b 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -14,32 +14,6 @@ window.Lidarr = await response.json(); __webpack_public_path__ = `${window.Lidarr.urlBase}/`; /* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */ -const error = console.error; - -// Monkey patch console.error to filter out some warnings from React -// TODO: Remove this after the great TypeScript migration - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function logError(...parameters: any[]) { - const filter = parameters.find((parameter) => { - return ( - typeof parameter === 'string' && - (parameter.includes( - 'Support for defaultProps will be removed from function components in a future major release' - ) || - parameter.includes( - 'findDOMNode is deprecated and will be removed in the next major release' - )) - ); - }); - - if (!filter) { - error(...parameters); - } -} - -console.error = logError; - const { bootstrap } = await import('./bootstrap'); await bootstrap(); diff --git a/frontend/src/login.html b/frontend/src/login.html index 24d086959..a65106664 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -3,19 +3,13 @@ + + - - - - - @@ -36,11 +30,7 @@ sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png" /> - + - + @@ -61,9 +54,9 @@