mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-23 23:05:18 -07:00
Compare commits
No commits in common. "develop" and "v1.4.3.3586" have entirely different histories.
develop
...
v1.4.3.358
1307 changed files with 23817 additions and 45745 deletions
|
@ -1,13 +0,0 @@
|
|||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Lidarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "20",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8686],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -60,7 +60,6 @@ body:
|
|||
- Master
|
||||
- Develop
|
||||
- Nightly
|
||||
- Plugins (experimental)
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
required: true
|
||||
|
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
|
@ -1,12 +0,0 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
16
.github/label-actions.yml
vendored
16
.github/label-actions.yml
vendored
|
@ -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).
|
17
.github/workflows/label-actions.yml
vendored
17
.github/workflows/label-actions.yml
vendored
|
@ -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'
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
|
|
32
.github/workflows/support.yml
vendored
Normal file
32
.github/workflows/support.yml
vendored
Normal file
|
@ -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
|
36
.gitignore
vendored
36
.gitignore
vendored
|
@ -121,13 +121,11 @@ _artifacts
|
|||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
@ -140,6 +138,12 @@ project.fragment.lock.json
|
|||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
#VS outout folders
|
||||
bin
|
||||
obj
|
||||
output/*
|
||||
|
||||
|
||||
# macOS metadata files
|
||||
._*
|
||||
.DS_Store
|
||||
|
@ -158,12 +162,34 @@ Thumbs.db
|
|||
/tools/Addins/*
|
||||
packages.config.md5sum
|
||||
|
||||
|
||||
# Common IntelliJ Platform excludes
|
||||
|
||||
# User specific
|
||||
**/.idea/**/workspace.xml
|
||||
**/.idea/**/tasks.xml
|
||||
**/.idea/shelf/*
|
||||
**/.idea/dictionaries
|
||||
**/.idea/.idea.Radarr.Posix
|
||||
**/.idea/.idea.Radarr.Windows
|
||||
|
||||
# Sensitive or high-churn files
|
||||
**/.idea/**/dataSources/
|
||||
**/.idea/**/dataSources.ids
|
||||
**/.idea/**/dataSources.xml
|
||||
**/.idea/**/dataSources.local.xml
|
||||
**/.idea/**/sqlDataSources.xml
|
||||
**/.idea/**/dynamic.xml
|
||||
|
||||
# Rider
|
||||
# Rider auto-generates .iml files, and contentModel.xml
|
||||
**/.idea/**/*.iml
|
||||
**/.idea/**/contentModel.xml
|
||||
**/.idea/**/modules.xml
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||
.idea/
|
||||
|
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Lidarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Lidarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
44
.vscode/tasks.json
vendored
44
.vscode/tasks.json
vendored
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Lidarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
# Lidarr
|
||||
|
||||
[](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/lidarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
|
@ -9,9 +8,6 @@
|
|||
|
||||
Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||
|
||||
> [!WARNING]
|
||||
> NOTICE - The Lidarr Metadata Server is 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.
|
||||
|
|
|
@ -9,18 +9,18 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.14.0'
|
||||
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:
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
#!/bin/bash
|
||||
### Description: Lidarr .NET Debian install
|
||||
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
|
||||
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
|
||||
### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Lidarr installs
|
||||
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
|
||||
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
|
||||
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
|
||||
|
||||
### Boilerplate Warning
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
scriptversion="1.0.3"
|
||||
scriptdate="2024-01-06"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Running Lidarr Install Script - Version [$scriptversion] as of [$scriptdate]"
|
||||
|
||||
# Am I root?, need root!
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root."
|
||||
exit
|
||||
fi
|
||||
|
||||
app="lidarr"
|
||||
app_port="8686"
|
||||
app_prereq="curl sqlite3 wget"
|
||||
app_umask="0002"
|
||||
branch="main"
|
||||
|
||||
# Constants
|
||||
### Update these variables as required for your specific instance
|
||||
installdir="/opt" # {Update me if needed} Install Location
|
||||
bindir="${installdir}/${app^}" # Full Path to Install Location
|
||||
datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use
|
||||
app_bin=${app^} # Binary Name of the app
|
||||
|
||||
# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir.
|
||||
if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then
|
||||
echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Prompt User
|
||||
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
|
||||
app_uid=$(echo "$app_uid" | tr -d ' ')
|
||||
app_uid=${app_uid:-$app}
|
||||
# Prompt Group
|
||||
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
|
||||
app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||
app_guid=${app_guid:-media}
|
||||
|
||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||
|
||||
# Create User / Group as needed
|
||||
if [ "$app_guid" != "$app_uid" ]; then
|
||||
if ! getent group "$app_guid" >/dev/null; then
|
||||
groupadd "$app_guid"
|
||||
fi
|
||||
fi
|
||||
if ! getent passwd "$app_uid" >/dev/null; then
|
||||
adduser --system --no-create-home --ingroup "$app_guid" "$app_uid"
|
||||
echo "Created and added User [$app_uid] to Group [$app_guid]"
|
||||
fi
|
||||
if ! getent group "$app_guid" | grep -qw "$app_uid"; then
|
||||
echo "User [$app_uid] did not exist in Group [$app_guid]"
|
||||
usermod -a -G "$app_guid" "$app_uid"
|
||||
echo "Added User [$app_uid] to Group [$app_guid]"
|
||||
fi
|
||||
|
||||
# Stop the App if running
|
||||
if service --status-all | grep -Fq "$app"; then
|
||||
systemctl stop "$app"
|
||||
systemctl disable "$app".service
|
||||
echo "Stopped existing $app"
|
||||
fi
|
||||
|
||||
# Create Appdata Directory
|
||||
|
||||
# AppData
|
||||
mkdir -p "$datadir"
|
||||
chown -R "$app_uid":"$app_guid" "$datadir"
|
||||
chmod 775 "$datadir"
|
||||
echo "Directories created"
|
||||
# Download and install the App
|
||||
|
||||
# prerequisite packages
|
||||
echo ""
|
||||
echo "Installing pre-requisite Packages"
|
||||
# shellcheck disable=SC2086
|
||||
apt update && apt install -y $app_prereq
|
||||
echo ""
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
# get arch
|
||||
dlbase="https://lidarr.servarr.com/v1/update/$branch/updatefile?os=linux&runtime=netcore"
|
||||
case "$ARCH" in
|
||||
"amd64") DLURL="${dlbase}&arch=x64" ;;
|
||||
"armhf") DLURL="${dlbase}&arch=arm" ;;
|
||||
"arm64") DLURL="${dlbase}&arch=arm64" ;;
|
||||
*)
|
||||
echo "Arch not supported"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
echo "Removing previous tarballs"
|
||||
# -f to Force so we fail if it doesn't exist
|
||||
rm -f "${app^}".*.tar.gz
|
||||
echo ""
|
||||
echo "Downloading..."
|
||||
wget --content-disposition "$DLURL"
|
||||
tar -xvzf "${app^}".*.tar.gz
|
||||
echo ""
|
||||
echo "Installation files downloaded and extracted"
|
||||
|
||||
# remove existing installs
|
||||
echo "Removing existing installation"
|
||||
rm -rf "$bindir"
|
||||
echo "Installing..."
|
||||
mv "${app^}" $installdir
|
||||
chown "$app_uid":"$app_guid" -R "$bindir"
|
||||
chmod 775 "$bindir"
|
||||
rm -rf "${app^}.*.tar.gz"
|
||||
# Ensure we check for an update in case user installs older version or different branch
|
||||
touch "$datadir"/update_required
|
||||
chown "$app_uid":"$app_guid" "$datadir"/update_required
|
||||
echo "App Installed"
|
||||
# Configure Autostart
|
||||
|
||||
# Remove any previous app .service
|
||||
echo "Removing old service file"
|
||||
rm -rf /etc/systemd/system/"$app".service
|
||||
|
||||
# Create app .service with correct user startup
|
||||
echo "Creating service file"
|
||||
cat <<EOF | tee /etc/systemd/system/"$app".service >/dev/null
|
||||
[Unit]
|
||||
Description=${app^} Daemon
|
||||
After=syslog.target network.target
|
||||
[Service]
|
||||
User=$app_uid
|
||||
Group=$app_guid
|
||||
UMask=$app_umask
|
||||
Type=simple
|
||||
ExecStart=$bindir/$app_bin -nobrowser -data=$datadir
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Start the App
|
||||
echo "Service file created. Attempting to start the app"
|
||||
systemctl -q daemon-reload
|
||||
systemctl enable --now -q "$app"
|
||||
|
||||
# Finish Update/Installation
|
||||
host=$(hostname -I)
|
||||
ip_local=$(grep -oP '^\S*' <<<"$host")
|
||||
echo ""
|
||||
echo "Install complete"
|
||||
sleep 10
|
||||
STATUS="$(systemctl is-active "$app")"
|
||||
if [ "${STATUS}" = "active" ]; then
|
||||
echo "Browse to http://$ip_local:$app_port for the ${app^} GUI"
|
||||
else
|
||||
echo "${app^} failed to start"
|
||||
fi
|
||||
|
||||
# Exit
|
||||
exit 0
|
|
@ -1,20 +0,0 @@
|
|||
# This file is owned by the lidarr package, DO NOT MODIFY MANUALLY
|
||||
# Instead use 'dpkg-reconfigure -plow lidarr' to modify User/Group/UMask/-data
|
||||
# Or use systemd built-in override functionality using 'systemctl edit lidarr'
|
||||
[Unit]
|
||||
Description=Lidarr Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=lidarr
|
||||
Group=lidarr
|
||||
UMask=002
|
||||
|
||||
Type=simple
|
||||
ExecStart=/opt/Lidarr/Lidarr -nobrowser -data=/var/lib/lidarr
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
23
docs.sh
23
docs.sh
|
@ -1,18 +1,13 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
ARCHITECTURE="${2:-x64}"
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-$ARCHITECTURE"
|
||||
RUNTIME="win-x64"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
RUNTIME="linux-$ARCHITECTURE"
|
||||
RUNTIME="linux-x64"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
RUNTIME="osx-$ARCHITECTURE"
|
||||
RUNTIME="osx-x64"
|
||||
else
|
||||
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
@ -26,21 +21,15 @@ slnFile=src/Lidarr.sln
|
|||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Lidarr.Console.dll
|
||||
else
|
||||
application=Lidarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/lidarr.console.dll" v1 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
|
|
@ -28,8 +28,7 @@ module.exports = {
|
|||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false,
|
||||
JSX: true
|
||||
sinon: false
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
|
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
|
@ -9,7 +9,7 @@
|
|||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
"source.fixAll": true
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
|
|
@ -2,8 +2,6 @@ const loose = true;
|
|||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (env) => {
|
|||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
|
@ -68,7 +67,7 @@ module.exports = (env) => {
|
|||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
filename: '[name]-[contenthash].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
|
@ -93,7 +92,7 @@ module.exports = (env) => {
|
|||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
|
@ -135,12 +134,6 @@ module.exports = (env) => {
|
|||
{
|
||||
source: 'frontend/src/Content/robots.txt',
|
||||
destination: path.join(distFolder, 'Content/robots.txt')
|
||||
},
|
||||
|
||||
// manifest.json and browserconfig.xml
|
||||
{
|
||||
source: 'frontend/src/Content/*.(json|xml)',
|
||||
destination: path.join(distFolder, 'Content')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -188,7 +181,7 @@ module.exports = (env) => {
|
|||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: '3.41'
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -209,7 +202,7 @@ module.exports = (env) => {
|
|||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -16,7 +16,6 @@ const mixinsFiles = [
|
|||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
|
|
@ -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 {
|
|||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={this.onClearBlocklistPress}
|
||||
onPress={onClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
|
@ -235,16 +220,6 @@ class Blocklist extends Component {
|
|||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmClearModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ClearBlocklist')}
|
||||
message={translate('ClearBlocklistMessageText')}
|
||||
confirmLabel={translate('Clear')}
|
||||
onConfirm={this.onClearBlocklistConfirmed}
|
||||
onCancel={this.onConfirmClearModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 &&
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
releaseGroup ?
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('ReleaseGroup')}
|
||||
data={releaseGroup}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -121,7 +119,7 @@ function HistoryDetails(props) {
|
|||
nzbInfoUrl ?
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
{translate('InfoUrl')}
|
||||
Info URL
|
||||
</DescriptionListItemTitle>
|
||||
|
||||
<DescriptionListItemDescription>
|
||||
|
@ -141,30 +139,27 @@ function HistoryDetails(props) {
|
|||
}
|
||||
|
||||
{
|
||||
downloadId ?
|
||||
!!downloadId &&
|
||||
<DescriptionListItem
|
||||
title={translate('GrabId')}
|
||||
title={translate('GrabID')}
|
||||
data={downloadId}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
age || ageHours || ageMinutes ?
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title={translate('AgeWhenGrabbed')}
|
||||
data={formatAge(age, ageHours, ageMinutes)}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
publishedDate ?
|
||||
!!publishedDate &&
|
||||
<DescriptionListItem
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
@ -172,8 +167,7 @@ function HistoryDetails(props) {
|
|||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message,
|
||||
indexer
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
|
@ -185,29 +179,11 @@ function HistoryDetails(props) {
|
|||
/>
|
||||
|
||||
{
|
||||
downloadId ?
|
||||
<DescriptionListItem
|
||||
title={translate('GrabId')}
|
||||
data={downloadId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
indexer ? (
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{
|
||||
message ?
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
@ -229,13 +205,12 @@ function HistoryDetails(props) {
|
|||
/>
|
||||
|
||||
{
|
||||
droppedPath ?
|
||||
!!droppedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Source')}
|
||||
data={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 &&
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
releaseGroup ?
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
title={translate('ReleaseGroup')}
|
||||
data={releaseGroup}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
customFormatScore && customFormatScore !== '0' ?
|
||||
<DescriptionListItem
|
||||
title={translate('CustomFormatScore')}
|
||||
data={formatCustomFormatScore(customFormatScore)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
nzbInfoUrl ?
|
||||
!!nzbInfoUrl &&
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
{translate('InfoUrl')}
|
||||
Info URL
|
||||
</DescriptionListItemTitle>
|
||||
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
</span> :
|
||||
null
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
downloadClient ?
|
||||
!!downloadClient &&
|
||||
<DescriptionListItem
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClient}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
downloadId ?
|
||||
!!downloadId &&
|
||||
<DescriptionListItem
|
||||
title={translate('GrabId')}
|
||||
title={translate('GrabID')}
|
||||
data={downloadId}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
age || ageHours || ageMinutes ?
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title={translate('AgeWhenGrabbed')}
|
||||
data={formatAge(age, ageHours, ageMinutes)}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
publishedDate ?
|
||||
!!publishedDate &&
|
||||
<DescriptionListItem
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
@ -495,21 +454,11 @@ function HistoryDetails(props) {
|
|||
/>
|
||||
|
||||
{
|
||||
downloadId ?
|
||||
<DescriptionListItem
|
||||
title={translate('GrabId')}
|
||||
data={downloadId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
message ?
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
|
@ -259,11 +243,7 @@ class Queue extends Component {
|
|||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey !== 'all' && count > 0 ?
|
||||
translate('QueueFilterHasNoItems') :
|
||||
translate('QueueIsEmpty')
|
||||
}
|
||||
{translate('QueueIsEmpty')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
@ -309,16 +289,9 @@ class Queue extends Component {
|
|||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
@ -326,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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -26,5 +26,4 @@
|
|||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -98,10 +98,8 @@ class QueueRow extends Component {
|
|||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
downloadForced,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
|
@ -330,15 +328,6 @@ class QueueRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
@ -404,9 +393,7 @@ class QueueRow extends Component {
|
|||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!artist}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
171
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
171
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
|
@ -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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove - {sourceTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFromClient"
|
||||
value={removeFromClient}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveFromClientChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
|
@ -1,231 +0,0 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
removeFromClient: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
skipRedownload: boolean;
|
||||
}
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
selectedCount?: number;
|
||||
onRemovePress(props: RemovePressProps): void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
selectedCount,
|
||||
onRemovePress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedCount === 1) {
|
||||
return {
|
||||
title: translate('RemoveSelectedItem'),
|
||||
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: translate('RemoveSelectedItems'),
|
||||
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||
selectedCount,
|
||||
}),
|
||||
};
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
hint: multipleSelected
|
||||
? translate('RemoveMultipleFromDownloadClientHint')
|
||||
: translate('RemoveFromDownloadClientHint'),
|
||||
},
|
||||
{
|
||||
key: 'changeCategory',
|
||||
value: translate('ChangeCategory'),
|
||||
isDisabled: !canChangeCategory,
|
||||
hint: multipleSelected
|
||||
? translate('ChangeCategoryMultipleHint')
|
||||
: translate('ChangeCategoryHint'),
|
||||
},
|
||||
{
|
||||
key: 'ignore',
|
||||
value: multipleSelected
|
||||
? translate('IgnoreDownloads')
|
||||
: translate('IgnoreDownload'),
|
||||
isDisabled: !canIgnore,
|
||||
hint: multipleSelected
|
||||
? translate('IgnoreDownloadsHint')
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
hint: translate('DoNotBlocklistHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
isDisabled: isPending,
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [isPending, multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
removeFromClient: removalMethod === 'removeFromClient',
|
||||
changeCategory: removalMethod === 'changeCategory',
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>{message}</div>
|
||||
|
||||
{isPending ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="removalMethod"
|
||||
value={removalMethod}
|
||||
values={removalMethodOptions}
|
||||
isDisabled={!canChangeCategory && !canIgnore}
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{multipleSelected
|
||||
? translate('BlocklistReleases')
|
||||
: translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="blocklistMethod"
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveQueueItemModal;
|
172
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
172
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
|
@ -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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', [selectedCount]) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFromClient"
|
||||
value={removeFromClient}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveFromClientChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
|
@ -1,9 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
|
@ -28,13 +25,11 @@ function TimeleftCell(props) {
|
|||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={translate('DelayingDownloadUntil', { date, time })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('DelayingDownloadUntilInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -44,13 +39,11 @@ function TimeleftCell(props) {
|
|||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
tooltip={translate('RetryingDownloadOn', { date, time })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('RetryingDownloadInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() {
|
|||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllAlbums')}
|
||||
data={translate('MonitorAllAlbums')}
|
||||
data="Monitor all new albums"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('NewAlbums')}
|
||||
data={translate('MonitorNewAlbumsData')}
|
||||
data="Monitor new albums released after the newest existing album"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data={translate('MonitorNoAlbumsData')}
|
||||
data="Don't monitor any new albums"
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
|
@ -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 (
|
||||
<>
|
||||
<Alert kind={kinds.INFO} className={styles.message}>
|
||||
<Alert>
|
||||
This is a one time adjustment to set which albums are monitored
|
||||
</Alert>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllAlbums')}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import Artist from 'Artist/Artist';
|
||||
|
||||
export interface Statistics {
|
||||
trackCount: number;
|
||||
trackFileCount: number;
|
||||
percentOfTracks: number;
|
||||
sizeOnDisk: number;
|
||||
totalTrackCount: number;
|
||||
}
|
||||
|
||||
interface Album extends ModelBase {
|
||||
artistId: number;
|
||||
artist: Artist;
|
||||
foreignAlbumId: string;
|
||||
title: string;
|
||||
overview: string;
|
||||
disambiguation?: string;
|
||||
albumType: string;
|
||||
monitored: boolean;
|
||||
releaseDate: string;
|
||||
statistics: Statistics;
|
||||
lastSearchTime?: string;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default Album;
|
|
@ -4,7 +4,6 @@ import IconButton from 'Components/Link/IconButton';
|
|||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumInteractiveSearchModalConnector from './Search/AlbumInteractiveSearchModalConnector';
|
||||
import styles from './AlbumSearchCell.css';
|
||||
|
||||
|
@ -50,13 +49,11 @@ class AlbumSearchCell extends Component {
|
|||
name={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
onPress={onSearchPress}
|
||||
title={translate('AutomaticSearch')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onManualSearchPress}
|
||||
title={translate('InteractiveSearch')}
|
||||
/>
|
||||
|
||||
<AlbumInteractiveSearchModalConnector
|
||||
|
|
|
@ -4,11 +4,10 @@ import Link from 'Components/Link/Link';
|
|||
|
||||
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
|
||||
const link = `/album/${foreignAlbumId}`;
|
||||
const albumTitle = `${title}${disambiguation ? ` (${disambiguation})` : ''}`;
|
||||
|
||||
return (
|
||||
<Link to={link} title={albumTitle}>
|
||||
{albumTitle}
|
||||
<Link to={link}>
|
||||
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,13 +53,13 @@ class DeleteAlbumModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
title,
|
||||
statistics = {},
|
||||
statistics,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
trackFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
|
@ -133,14 +133,14 @@ class DeleteAlbumModalContent extends Component {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteAlbumConfirmed}
|
||||
>
|
||||
{translate('Delete')}
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
@ -119,10 +119,7 @@
|
|||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.releaseDate,
|
||||
.sizeOnDisk,
|
||||
.albumType,
|
||||
.secondaryTypes,
|
||||
.qualityProfileName,
|
||||
.links,
|
||||
.tags {
|
||||
|
@ -150,12 +147,6 @@
|
|||
.headerContent {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
|||
<div className={styles.details}>
|
||||
<div>
|
||||
{
|
||||
duration ?
|
||||
!!duration &&
|
||||
<span className={styles.duration}>
|
||||
{formatDuration(duration)}
|
||||
</span> :
|
||||
null
|
||||
</span>
|
||||
}
|
||||
|
||||
<HeartRating
|
||||
|
@ -418,18 +418,18 @@ class AlbumDetails extends Component {
|
|||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('ReleaseDate')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.releaseDate}>
|
||||
{moment(releaseDate).format(shortDateFormat)}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
moment(releaseDate).format(shortDateFormat)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
|
@ -438,15 +438,16 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -462,55 +463,32 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
{
|
||||
albumType ?
|
||||
!!albumType &&
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('Type')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.albumType}>
|
||||
{albumType}
|
||||
</span>
|
||||
</div>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
{
|
||||
secondaryTypes.length ?
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('SecondaryTypes')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.secondaryTypes}>
|
||||
{secondaryTypes.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</Label> :
|
||||
null
|
||||
<span className={styles.qualityProfileName}>
|
||||
{albumType}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
||||
<Tooltip
|
||||
|
@ -519,15 +497,14 @@ class AlbumDetails extends Component {
|
|||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.links}>
|
||||
{translate('Links')}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
Links
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
|
@ -553,38 +530,28 @@ class AlbumDetails extends Component {
|
|||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !albumsError && !tracksError && !trackFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
!isPopulated && !albumsError && !trackFilesError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && albumsError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AlbumsLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
!isFetching && albumsError &&
|
||||
<div>
|
||||
{translate('LoadingAlbumsFailed')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && tracksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('TracksLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && trackFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('TrackFilesLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
!isFetching && trackFilesError &&
|
||||
<div>
|
||||
{translate('LoadingTrackFilesFailed')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!media.length &&
|
||||
<div>
|
||||
|
||||
{
|
||||
media.slice(0).map((medium) => {
|
||||
return (
|
||||
|
@ -602,14 +569,6 @@ class AlbumDetails extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !media.length ?
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('NoMediumInformation')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
|
@ -676,7 +635,6 @@ AlbumDetails.propTypes = {
|
|||
duration: PropTypes.number,
|
||||
overview: PropTypes.string,
|
||||
albumType: PropTypes.string.isRequired,
|
||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
|
@ -703,8 +661,6 @@ AlbumDetails.propTypes = {
|
|||
};
|
||||
|
||||
AlbumDetails.defaultProps = {
|
||||
secondaryTypes: [],
|
||||
statistics: {},
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
|
|
|
@ -70,12 +70,6 @@ function createMapStateToProps() {
|
|||
isCommandExecuting(isSearchingCommand) &&
|
||||
isSearchingCommand.body.albumIds.indexOf(album.id) > -1
|
||||
);
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
|
||||
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
||||
const isRenamingArtist = (
|
||||
isCommandExecuting(isRenamingArtistCommand) &&
|
||||
isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1
|
||||
);
|
||||
|
||||
const isFetching = tracks.isFetching || isTrackFilesFetching;
|
||||
const isPopulated = tracks.isPopulated && isTrackFilesPopulated;
|
||||
|
@ -86,8 +80,6 @@ function createMapStateToProps() {
|
|||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
artist,
|
||||
isSearching,
|
||||
isRenamingFiles,
|
||||
isRenamingArtist,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
tracksError,
|
||||
|
@ -121,27 +113,8 @@ class AlbumDetailsConnector extends Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
anyReleaseOk,
|
||||
isRenamingFiles,
|
||||
isRenamingArtist
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||
(prevProps.isRenamingArtist && !isRenamingArtist) ||
|
||||
!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && anyReleaseOk === true)
|
||||
) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
|
||||
// If the id has changed we need to clear the album
|
||||
// files and fetch from the server.
|
||||
|
||||
if (prevProps.id !== id) {
|
||||
if (!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
|
@ -201,8 +174,6 @@ class AlbumDetailsConnector extends Component {
|
|||
AlbumDetailsConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
anyReleaseOk: PropTypes.bool,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingArtist: PropTypes.bool.isRequired,
|
||||
isAlbumFetching: PropTypes.bool,
|
||||
isAlbumPopulated: PropTypes.bool,
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
|
|
|
@ -12,13 +12,16 @@ import TrackRowConnector from './TrackRowConnector';
|
|||
import styles from './AlbumDetailsMedium.css';
|
||||
|
||||
function getMediumStatistics(tracks) {
|
||||
const trackCount = tracks.length;
|
||||
let trackCount = 0;
|
||||
let trackFileCount = 0;
|
||||
let totalTrackCount = 0;
|
||||
|
||||
tracks.forEach((track) => {
|
||||
if (track.trackFileId) {
|
||||
trackCount++;
|
||||
trackFileCount++;
|
||||
} else {
|
||||
trackCount++;
|
||||
}
|
||||
|
||||
totalTrackCount++;
|
||||
|
@ -172,7 +175,7 @@ class AlbumDetailsMedium extends Component {
|
|||
</Table> :
|
||||
|
||||
<div className={styles.noTracks}>
|
||||
{translate('NoTracksInThisMedium')}
|
||||
No tracks in this medium
|
||||
</div>
|
||||
}
|
||||
<div className={styles.collapseButtonContainer}>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
2
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
2
frontend/src/Album/Details/TrackRow.css.d.ts
vendored
|
@ -4,9 +4,7 @@ interface CssExports {
|
|||
'audio': string;
|
||||
'customFormatScore': string;
|
||||
'duration': string;
|
||||
'indexerFlags': string;
|
||||
'monitored': string;
|
||||
'size': string;
|
||||
'status': string;
|
||||
'title': string;
|
||||
'trackNumber': string;
|
||||
|
|
|
@ -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={<AlbumFormats formats={customFormats} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexerFlags') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexerFlags}
|
||||
>
|
||||
{indexerFlags ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.size}
|
||||
>
|
||||
{!!trackFileSize && formatBytes(trackFileSize)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
@ -228,17 +192,14 @@ TrackRow.propTypes = {
|
|||
duration: PropTypes.number.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
trackFilePath: PropTypes.string,
|
||||
trackFileSize: PropTypes.number,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
mediaInfo: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
TrackRow.defaultProps = {
|
||||
customFormats: [],
|
||||
indexerFlags: 0
|
||||
customFormats: []
|
||||
};
|
||||
|
||||
export default TrackRow;
|
||||
|
|
|
@ -11,10 +11,8 @@ function createMapStateToProps() {
|
|||
(id, trackFile) => {
|
||||
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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
|
|||
title,
|
||||
artistName,
|
||||
albumType,
|
||||
statistics = {},
|
||||
statistics,
|
||||
item,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
|
@ -43,10 +43,6 @@ class EditAlbumModalContent extends Component {
|
|||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
trackFileCount = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
anyReleaseOk,
|
||||
|
@ -100,7 +96,7 @@ class EditAlbumModalContent extends Component {
|
|||
type={inputTypes.ALBUM_RELEASE_SELECT}
|
||||
name="releases"
|
||||
helpText={translate('ReleasesHelpText')}
|
||||
isDisabled={anyReleaseOk.value && trackFileCount > 0}
|
||||
isDisabled={anyReleaseOk.value && statistics.trackFileCount > 0}
|
||||
albumReleases={releases}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
||||
|
||||
interface IndexerFlagsProps {
|
||||
indexerFlags: number;
|
||||
}
|
||||
|
||||
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
||||
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
||||
|
||||
const flags = allIndexerFlags.items.filter(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(item) => (indexerFlags & item.id) === item.id
|
||||
);
|
||||
|
||||
return flags.length ? (
|
||||
<ul>
|
||||
{flags.map((flag, index) => {
|
||||
return <li key={index}>{flag.name}</li>;
|
||||
})}
|
||||
</ul>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default IndexerFlags;
|
|
@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) {
|
|||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -7,7 +7,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AlbumInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
|
@ -19,10 +18,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{albumTitle === undefined ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
|
||||
}
|
||||
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
|
@ -36,7 +32,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function getTooltip(title, quality, size) {
|
||||
if (!title) {
|
||||
|
@ -27,44 +26,13 @@ function getTooltip(title, quality, size) {
|
|||
return title;
|
||||
}
|
||||
|
||||
function revisionLabel(className, quality, showRevision) {
|
||||
if (!showRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (quality.revision.isRepack) {
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kinds.PRIMARY}
|
||||
title={translate('Repack')}
|
||||
>
|
||||
R
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
if (quality.revision.version && quality.revision.version > 1) {
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kinds.PRIMARY}
|
||||
title={translate('Proper')}
|
||||
>
|
||||
P
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TrackQuality(props) {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
quality,
|
||||
size,
|
||||
isCutoffNotMet,
|
||||
showRevision
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
|
||||
if (!quality) {
|
||||
|
@ -72,15 +40,13 @@ function TrackQuality(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Label
|
||||
className={className}
|
||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||
title={getTooltip(title, quality, size)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>{revisionLabel(className, quality, showRevision)}
|
||||
</span>
|
||||
<Label
|
||||
className={className}
|
||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||
title={getTooltip(title, quality, size)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -89,13 +55,11 @@ TrackQuality.propTypes = {
|
|||
title: PropTypes.string,
|
||||
quality: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
isCutoffNotMet: PropTypes.bool,
|
||||
showRevision: PropTypes.bool
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
};
|
||||
|
||||
TrackQuality.defaultProps = {
|
||||
title: '',
|
||||
showRevision: false
|
||||
title: ''
|
||||
};
|
||||
|
||||
export default TrackQuality;
|
||||
|
|
36
frontend/src/AlbumStudio/AlbumStudio.css
Normal file
36
frontend/src/AlbumStudio/AlbumStudio.css
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
443
frontend/src/AlbumStudio/AlbumStudio.js
Normal file
443
frontend/src/AlbumStudio/AlbumStudio.js
Normal file
|
@ -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 (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={this.cache}
|
||||
parent={parent}
|
||||
columnIndex={0}
|
||||
rowIndex={rowIndex}
|
||||
>
|
||||
{({ registerChild }) => (
|
||||
<VirtualTableRow
|
||||
ref={registerChild}
|
||||
style={style}
|
||||
>
|
||||
<AlbumStudioRowConnector
|
||||
key={item.id}
|
||||
artistId={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// 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 (
|
||||
<PageContent title={translate('AlbumStudio')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection />
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={AlbumStudioFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles.innerContentBody}
|
||||
>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>{getErrorMessage(error, 'Failed to load artist from API')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !!items.length &&
|
||||
<div className={styles.contentBodyContainer}>
|
||||
<VirtualTable
|
||||
items={items}
|
||||
scrollIndex={scrollIndex}
|
||||
columns={columns}
|
||||
scroller={scroller}
|
||||
isSmallScreen={isSmallScreen}
|
||||
overscanRowCount={5}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
<AlbumStudioTableHeader
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
deferredMeasurementCache={this.cache}
|
||||
rowHeight={this.cache.rowHeight}
|
||||
estimatedRowSize={estimatedRowSize}
|
||||
onRecompute={this.onGridRecompute}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoArtist totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isPopulated && !!jumpBarItems.order.length &&
|
||||
<PageJumpBar
|
||||
items={jumpBarItems}
|
||||
onItemPress={this.onJumpBarItemPress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<AlbumStudioFooter
|
||||
selectedCount={this.getSelectedIds().length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
102
frontend/src/AlbumStudio/AlbumStudioAlbum.js
Normal file
102
frontend/src/AlbumStudio/AlbumStudioAlbum.js
Normal file
|
@ -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 (
|
||||
<div className={styles.album}>
|
||||
<div className={styles.info}>
|
||||
<MonitorToggleButton
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
onPress={this.onAlbumMonitoredPress}
|
||||
/>
|
||||
|
||||
<span>
|
||||
{
|
||||
disambiguation ? `${title} (${disambiguation})` : `${title}`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.albumType}>
|
||||
<span>
|
||||
{
|
||||
`${albumType}`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
styles.tracks,
|
||||
percentOfTracks < 100 && monitored && styles.missingWanted,
|
||||
percentOfTracks === 100 && styles.allTracks
|
||||
)}
|
||||
title={translate('TrackFileCounttotalTrackCountTracksDownloadedInterp', [trackFileCount, totalTrackCount])}
|
||||
>
|
||||
{
|
||||
totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
116
frontend/src/AlbumStudio/AlbumStudioConnector.js
Normal file
116
frontend/src/AlbumStudio/AlbumStudioConnector.js
Normal file
|
@ -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 (
|
||||
<AlbumStudio
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
24
frontend/src/AlbumStudio/AlbumStudioFilterModalConnector.js
Normal file
24
frontend/src/AlbumStudio/AlbumStudioFilterModalConnector.js
Normal file
|
@ -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);
|
14
frontend/src/AlbumStudio/AlbumStudioFooter.css
Normal file
14
frontend/src/AlbumStudio/AlbumStudioFooter.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -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;
|
174
frontend/src/AlbumStudio/AlbumStudioFooter.js
Normal file
174
frontend/src/AlbumStudio/AlbumStudioFooter.js
Normal file
|
@ -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 (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
{translate('MonitorArtist')}
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
{translate('MonitorExistingAlbums')}
|
||||
</div>
|
||||
|
||||
<MonitorAlbumsSelectInput
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
includeNoChange={true}
|
||||
includeNoChangeDisabled={false}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
{translate('MonitorNewAlbums')}
|
||||
</div>
|
||||
|
||||
<MonitorNewItemsSelectInput
|
||||
name="monitorNewItems"
|
||||
value={monitorNewItems}
|
||||
includeNoChange={true}
|
||||
includeNoChangeDisabled={false}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.label}>
|
||||
{translate('CountArtistsSelected', { selectedCount })}
|
||||
</div>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.updateSelectedButton}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!selectedCount || noChanges}
|
||||
onPress={this.onUpdateSelectedPress}
|
||||
>
|
||||
{translate('UpdateSelected')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumStudioFooter.propTypes = {
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
onUpdateSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AlbumStudioFooter;
|
41
frontend/src/AlbumStudio/AlbumStudioRow.css
Normal file
41
frontend/src/AlbumStudio/AlbumStudioRow.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -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;
|
95
frontend/src/AlbumStudio/AlbumStudioRow.js
Normal file
95
frontend/src/AlbumStudio/AlbumStudioRow.js
Normal file
|
@ -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 (
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
className={styles.selectCell}
|
||||
id={artistId}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isDisabled={false}
|
||||
/>
|
||||
|
||||
<ArtistStatusCell
|
||||
className={styles.status}
|
||||
artistType={artistType}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
isSaving={isSaving}
|
||||
onMonitoredPress={onArtistMonitoredPress}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
|
||||
<VirtualTableRowCell className={styles.title}>
|
||||
<ArtistNameLink
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.albums}>
|
||||
{
|
||||
albums.map((album) => {
|
||||
return (
|
||||
<AlbumStudioAlbum
|
||||
key={album.id}
|
||||
{...album}
|
||||
onAlbumMonitoredPress={onAlbumMonitoredPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
94
frontend/src/AlbumStudio/AlbumStudioRowConnector.js
Normal file
94
frontend/src/AlbumStudio/AlbumStudioRowConnector.js
Normal file
|
@ -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 (
|
||||
<AlbumStudioRow
|
||||
{...this.props}
|
||||
onArtistMonitoredPress={this.onArtistMonitoredPress}
|
||||
onAlbumMonitoredPress={this.onAlbumMonitoredPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumStudioRowConnector.propTypes = {
|
||||
artistId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
toggleArtistMonitored: PropTypes.func.isRequired,
|
||||
toggleAlbumsMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector);
|
18
frontend/src/AlbumStudio/AlbumStudioTableHeader.css
Normal file
18
frontend/src/AlbumStudio/AlbumStudioTableHeader.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -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;
|
61
frontend/src/AlbumStudio/AlbumStudioTableHeader.js
Normal file
61
frontend/src/AlbumStudio/AlbumStudioTableHeader.js
Normal file
|
@ -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 (
|
||||
<VirtualTableHeader>
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
isSortable,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={isSortable}
|
||||
{...otherProps}
|
||||
>
|
||||
{typeof label === 'function' ? label() : label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
AlbumStudioTableHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AlbumStudioTableHeader;
|
|
@ -12,10 +12,11 @@ function App({ store, history }) {
|
|||
<DocumentTitle title={window.Lidarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
|
|
@ -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) {
|
|||
<Route
|
||||
exact={true}
|
||||
path="/"
|
||||
component={ArtistIndex}
|
||||
component={ArtistIndexConnector}
|
||||
/>
|
||||
|
||||
{
|
||||
|
@ -76,28 +78,12 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/artisteditor"
|
||||
exact={true}
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect
|
||||
to={getPathWithUrlBase('/')}
|
||||
component={app}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
component={ArtistEditorConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/albumstudio"
|
||||
exact={true}
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect
|
||||
to={getPathWithUrlBase('/')}
|
||||
component={app}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
component={AlbumStudioConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
@ -184,7 +170,7 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/settings/customformats"
|
||||
component={CustomFormatSettingsPage}
|
||||
component={CustomFormatSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
@ -248,7 +234,7 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={Updates}
|
||||
component={UpdatesConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
|
|
@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AppUpdated')}
|
||||
{translate('AppUpdated', { appName: 'Lidarr' })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Lidarr', version })} blockClassName={styles.version} />
|
||||
</div>
|
||||
|
||||
{
|
||||
|
|
50
frontend/src/App/ApplyTheme.js
Normal file
50
frontend/src/App/ApplyTheme.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
@ -1,37 +0,0 @@
|
|||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
interface ApplyThemeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
|
@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
|||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('ConnectionLostToBackend')}
|
||||
{translate('ConnectionLostToBackend', { appName: 'Lidarr' })}
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
{translate('ConnectionLostReconnect')}
|
||||
{translate('ConnectionLostReconnect', { appName: 'Lidarr' })}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
|
|
@ -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<T extends ModelBase> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
children: any;
|
||||
items: Array<T>;
|
||||
}
|
||||
|
||||
const SelectContext = React.createContext<
|
||||
[SelectState, SelectDispatch] | undefined
|
||||
>(cloneDeep(undefined));
|
||||
|
||||
export function SelectProvider<T extends ModelBase>(
|
||||
props: SelectProviderOptions<T>
|
||||
) {
|
||||
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 (
|
||||
<SelectContext.Provider value={value}>
|
||||
{props.children}
|
||||
</SelectContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSelect() {
|
||||
const context = React.useContext(SelectContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useSelect must be used within a SelectProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import Album from 'Album/Album';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
interface AlbumAppState extends AppSectionState<Album>, AppSectionDeleteState {}
|
||||
|
||||
export default AlbumAppState;
|
|
@ -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<T> {
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Artist>[];
|
||||
filters: Filter[];
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
interface ArtistAppState
|
||||
extends AppSectionState<Artist>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
|
||||
deleteOptions: {
|
||||
addImportListExclusion: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default ArtistAppState;
|
|
@ -1,10 +0,0 @@
|
|||
import Album from 'Album/Album';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
interface CalendarAppState
|
||||
extends AppSectionState<Album>,
|
||||
AppSectionFilterState<Album> {}
|
||||
|
||||
export default CalendarAppState;
|
|
@ -1,8 +0,0 @@
|
|||
import { CustomFilter } from './AppState';
|
||||
|
||||
interface ClientSideCollectionAppState {
|
||||
totalItems: number;
|
||||
customFilters: CustomFilter[];
|
||||
}
|
||||
|
||||
export default ClientSideCollectionAppState;
|
|
@ -1,6 +0,0 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
|
@ -1,10 +0,0 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { CustomFilter } from './AppState';
|
||||
|
||||
interface CustomFiltersAppState
|
||||
extends AppSectionState<CustomFilter>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export default CustomFiltersAppState;
|
|
@ -1,10 +0,0 @@
|
|||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History> {}
|
||||
|
||||
export default HistoryAppState;
|
|
@ -1,35 +0,0 @@
|
|||
import Album from 'Album/Album';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
export interface ArtistTitleInfo {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ParsedAlbumInfo {
|
||||
albumTitle: string;
|
||||
artistName: string;
|
||||
artistTitleInfo: ArtistTitleInfo;
|
||||
discography: boolean;
|
||||
quality: QualityModel;
|
||||
releaseGroup?: string;
|
||||
releaseHash: string;
|
||||
releaseTitle: string;
|
||||
releaseTokens: string;
|
||||
}
|
||||
|
||||
export interface ParseModel extends ModelBase {
|
||||
title: string;
|
||||
parsedAlbumInfo: ParsedAlbumInfo;
|
||||
artist?: Artist;
|
||||
albums: Album[];
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore?: number;
|
||||
}
|
||||
|
||||
type ParseAppState = AppSectionItemState<ParseModel>;
|
||||
|
||||
export default ParseAppState;
|
|
@ -1,27 +0,0 @@
|
|||
import Queue from 'typings/Queue';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
AppSectionItemState,
|
||||
Error,
|
||||
} from './AppSectionState';
|
||||
|
||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface QueuePagedAppState
|
||||
extends AppSectionState<Queue>,
|
||||
AppSectionFilterState<Queue> {
|
||||
isGrabbing: boolean;
|
||||
grabError: Error;
|
||||
isRemoving: boolean;
|
||||
removeError: Error;
|
||||
}
|
||||
|
||||
interface QueueAppState {
|
||||
status: AppSectionItemState<Queue>;
|
||||
details: QueueDetailsAppState;
|
||||
paged: QueuePagedAppState;
|
||||
}
|
||||
|
||||
export default QueueAppState;
|
|
@ -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<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
|
@ -37,40 +27,14 @@ export interface NotificationAppState
|
|||
extends AppSectionState<Notification>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface MetadataProfilesAppState
|
||||
extends AppSectionState<MetadataProfile>,
|
||||
AppSectionSchemaState<MetadataProfile> {}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface RootFolderAppState
|
||||
extends AppSectionState<RootFolder>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
customFormats: CustomFormatAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
metadataProfiles: MetadataProfilesAppState;
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
ui: UiSettingsAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue