mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-07 13:40:02 -05:00
Compare commits
1 Commits
develop
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc3d69a11 |
@@ -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": "Readarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8787],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -3,7 +3,8 @@
|
||||
|
||||
# Explicitly set bash scripts to have unix endings
|
||||
*.sh text eol=lf
|
||||
distribution/osx/Readarr text eol=lf
|
||||
distribution/debian/* text eol=lf
|
||||
macOS/Readarr text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Bug Report
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,3 +3,6 @@ contact_links:
|
||||
- name: Support via Discord
|
||||
url: https://readarr.com/discord
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
- name: Support via Reddit
|
||||
url: https://reddit.com/r/Readarr
|
||||
about: Discuss and search thru support topics.
|
||||
|
||||
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://readarr.com/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/readarr/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@v3
|
||||
with:
|
||||
process-only: 'issues'
|
||||
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://readarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/readarr)
|
||||
close-issue: true
|
||||
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/readarr/troubleshooting#logging-and-log-files).
|
||||
close-issue: false
|
||||
lock-issue: false
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -120,13 +120,11 @@ _artifacts
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
|
||||
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 Readarr",
|
||||
"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/Readarr",
|
||||
"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/Readarr.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/Readarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Readarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.0 KiB |
21
README.md
21
README.md
@@ -1,23 +1,3 @@
|
||||
# Announcement: Retirement of Readarr
|
||||
|
||||
We would like to announce that the [Readarr project](<https://github.com/Readarr/Readarr>) has been retired. This difficult decision was made due to a combination of factors: the project's metadata has become unusable, we no longer have the time to remake or repair it, and the community effort to transition to using Open Library as the source has stalled without much progress.
|
||||
|
||||
Third-party metadata mirrors exist, but as we're not involved with them at all, we cannot provide support for them. Use of them is entirely at your own risk. The most popular mirror appears to be [rreading-glasses](<https://github.com/blampe/rreading-glasses>).
|
||||
|
||||
Without anyone to take over Readarr development, we expect it to wither away, so we still encourage you to seek alternatives to Readarr.
|
||||
|
||||
## Key Points:
|
||||
- **Effective Immediately**: The retirement takes effect immediately. Please stay tuned for any possible further communications.
|
||||
- **Support Window**: We will provide support during a brief transition period to help with troubleshooting non metadata related issues.
|
||||
- **Alternative Solutions**: Users are encouraged to explore and adopt any other possible solutions as alternatives to Readarr.
|
||||
- **Opportunities for Revival**: We are open to someone taking over and revitalizing the project. If you are interested, please get in touch.
|
||||
- **Gratitude**: We extend our deepest gratitude to all the contributors and community members who supported Readarr over the years.
|
||||
|
||||
Thank you for being part of the Readarr journey. For any inquiries or assistance during this transition, please contact our team.
|
||||
|
||||
Sincerely,
|
||||
The Servarr Team
|
||||
|
||||
# Readarr
|
||||
|
||||
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||
@@ -50,6 +30,7 @@ Note that only one type of a given book is supported. If you want both an audiob
|
||||
|
||||
[](https://wiki.servarr.com/readarr)
|
||||
[](https://readarr.com/discord)
|
||||
[](https://www.reddit.com/r/readarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
|
||||
@@ -9,28 +9,24 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.4.19'
|
||||
majorVersion: '0.3.2'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.427'
|
||||
nodeVersion: '20.X'
|
||||
dotnetVersion: '6.0.408'
|
||||
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:
|
||||
include:
|
||||
- develop
|
||||
- master
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/Readarr.Api.*/openapi.json
|
||||
|
||||
pr:
|
||||
branches:
|
||||
@@ -38,37 +34,82 @@ pr:
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- .github
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Readarr.Api.*/openapi.json
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
displayName: Setup
|
||||
|
||||
- stage: Build_Backend_Windows
|
||||
displayName: Build Backend
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- job:
|
||||
displayName: Build Variables
|
||||
- job: Backend
|
||||
strategy:
|
||||
matrix:
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
vmImage: $(imageName)
|
||||
variables:
|
||||
# Disable stylecop here - linting errors get caught by the analyze task
|
||||
EnableAnalyzers: $(enableAnalysis)
|
||||
steps:
|
||||
# Set the build name properly. The 'name' property won't recursively expand so hack here:
|
||||
- bash: echo "##vso[build.updatebuildnumber]$READARRVERSION"
|
||||
displayName: Set Build Name
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
if [[ $BUILD_REASON == "PullRequest" ]]; then
|
||||
git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
|
||||
echo $? > not_backend_update
|
||||
else
|
||||
echo 0 > not_backend_update
|
||||
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
|
||||
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
cat not_backend_update
|
||||
displayName: Check for Backend File Changes
|
||||
- publish: not_backend_update
|
||||
artifact: not_backend_update
|
||||
displayName: Publish update type
|
||||
- stage: Build_Backend
|
||||
displayName: Build Backend
|
||||
dependsOn: Setup
|
||||
displayName: Extra Platform Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
path: $(nugetCacheFolder)
|
||||
displayName: Cache NuGet packages
|
||||
- bash: ./build.sh --backend --enable-bsd
|
||||
displayName: Build Readarr Backend
|
||||
env:
|
||||
NUGET_PACKAGES: $(nugetCacheFolder)
|
||||
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
|
||||
displayName: Clean up intermediate output
|
||||
- publish: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
|
||||
- stage: Build_Backend_Other
|
||||
displayName: Build Backend (Other OS)
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- job: Backend
|
||||
strategy:
|
||||
@@ -81,10 +122,6 @@ stages:
|
||||
osName: 'Mac'
|
||||
imageName: ${{ variables.macImage }}
|
||||
enableAnalysis: 'false'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
@@ -100,17 +137,22 @@ stages:
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
|
||||
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Enable Extra Platform Support
|
||||
displayName: Extra Platform Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
path: $(nugetCacheFolder)
|
||||
displayName: Cache NuGet packages
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
displayName: Build Readarr Backend
|
||||
env:
|
||||
NUGET_PACKAGES: $(nugetCacheFolder)
|
||||
- bash: |
|
||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
|
||||
@@ -118,38 +160,10 @@ stages:
|
||||
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
|
||||
displayName: Clean up intermediate output
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- publish: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
||||
- stage: Build_Frontend
|
||||
displayName: Frontend
|
||||
dependsOn: Setup
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- job: Build
|
||||
strategy:
|
||||
@@ -166,10 +180,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
|
||||
@@ -178,6 +192,7 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
@@ -189,10 +204,10 @@ stages:
|
||||
artifact: '$(osName)Frontend'
|
||||
displayName: Publish Frontend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
||||
|
||||
- stage: Installer
|
||||
dependsOn:
|
||||
- Build_Backend
|
||||
- Build_Backend_Windows
|
||||
- Build_Frontend
|
||||
jobs:
|
||||
- job: Windows_Installer
|
||||
@@ -216,8 +231,8 @@ stages:
|
||||
displayName: Fetch Frontend
|
||||
- bash: |
|
||||
./build.sh --packages --installer
|
||||
cp distribution/windows/setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp distribution/windows/setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create Installers
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
@@ -225,7 +240,7 @@ stages:
|
||||
|
||||
- stage: Packages
|
||||
dependsOn:
|
||||
- Build_Backend
|
||||
- Build_Backend_Windows
|
||||
- Build_Frontend
|
||||
jobs:
|
||||
- job: Other_Packages
|
||||
@@ -391,29 +406,14 @@ stages:
|
||||
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
|
||||
SENTRY_ORG: $(sentryOrg)
|
||||
SENTRY_URL: $(sentryUrl)
|
||||
|
||||
|
||||
- stage: Unit_Test
|
||||
displayName: Unit Tests
|
||||
dependsOn: Build_Backend
|
||||
|
||||
dependsOn: Build_Backend_Windows
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'not_backend_update'
|
||||
targetPath: '.'
|
||||
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
|
||||
name: setVar
|
||||
|
||||
- job: Unit
|
||||
displayName: Unit Native
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
workspace:
|
||||
clean: all
|
||||
|
||||
@@ -479,8 +479,6 @@ stages:
|
||||
|
||||
- job: Unit_Docker
|
||||
displayName: Unit Docker
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
@@ -494,11 +492,11 @@ stages:
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .NET'
|
||||
@@ -532,14 +530,12 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres14
|
||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: linux-x64-tests
|
||||
artifactName: LinuxCoreTests
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
@@ -549,7 +545,7 @@ stages:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
@@ -560,7 +556,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: $(artifactName)
|
||||
artifactName: 'linux-x64-Tests'
|
||||
targetPath: $(testsFolder)
|
||||
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
@@ -583,84 +579,15 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres15
|
||||
displayName: Unit Native LinuxCore with Postgres15 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: linux-x64-tests
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: $(artifactName)
|
||||
targetPath: $(testsFolder)
|
||||
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
docker run -d --name=postgres15 \
|
||||
-e POSTGRES_PASSWORD=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:15
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
dependsOn: Packages
|
||||
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'not_backend_update'
|
||||
targetPath: '.'
|
||||
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
|
||||
name: setVar
|
||||
|
||||
- job: Integration_Native
|
||||
displayName: Integration Native
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
strategy:
|
||||
matrix:
|
||||
MacCore:
|
||||
@@ -681,7 +608,7 @@ stages:
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
@@ -703,7 +630,7 @@ stages:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
@@ -722,10 +649,8 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres14
|
||||
displayName: Integration Native LinuxCore with Postgres14 Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
@@ -757,7 +682,7 @@ stages:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
@@ -780,77 +705,12 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
|
||||
- job: Integration_LinuxCore_Postgres15
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'linux-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
mkdir -p ./bin/
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
docker run -d --name=postgres15 \
|
||||
-e POSTGRES_PASSWORD=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:15
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_FreeBSD
|
||||
displayName: Integration Native FreeBSD
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
workspace:
|
||||
clean: all
|
||||
variables:
|
||||
@@ -895,8 +755,6 @@ stages:
|
||||
|
||||
- job: Integration_Docker
|
||||
displayName: Integration Docker
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
@@ -915,7 +773,7 @@ stages:
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
timeoutInMinutes: 15
|
||||
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .NET'
|
||||
@@ -943,7 +801,7 @@ stages:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
@@ -965,7 +823,7 @@ stages:
|
||||
- stage: Automation
|
||||
displayName: Automation
|
||||
dependsOn: Packages
|
||||
|
||||
|
||||
jobs:
|
||||
- job: Automation
|
||||
strategy:
|
||||
@@ -975,23 +833,20 @@ stages:
|
||||
artifactName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
artifactName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Readarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
artifactName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Readarr.*.windows-core-x64.zip'
|
||||
failBuild: true
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
@@ -1013,7 +868,7 @@ stages:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
@@ -1033,35 +888,20 @@ stages:
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
|
||||
- publish: $(Build.ArtifactStagingDirectory)/screenshots
|
||||
artifact: '$(osName)AutomationScreenshots'
|
||||
displayName: Publish Screenshot Bundle
|
||||
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
|
||||
displayName: Publish Screenshot Bundle
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(osName) Automation Tests'
|
||||
failTaskOnFailedTests: $(failBuild)
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- stage: Analyze
|
||||
dependsOn:
|
||||
- Setup
|
||||
dependsOn: []
|
||||
displayName: Analyze
|
||||
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'not_backend_update'
|
||||
targetPath: '.'
|
||||
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
|
||||
name: setVar
|
||||
|
||||
- job: Lint_Frontend
|
||||
displayName: Lint Frontend
|
||||
strategy:
|
||||
@@ -1075,10 +915,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
|
||||
@@ -1087,6 +927,7 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
@@ -1102,29 +943,24 @@ 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: 'readarr'
|
||||
scannerMode: 'cli'
|
||||
scannerMode: 'CLI'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'readarrui'
|
||||
cliProjectName: 'ReadarrUI'
|
||||
cliProjectVersion: '$(readarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@3
|
||||
|
||||
- task: SonarCloudAnalyze@1
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
dependsOn: Prepare
|
||||
condition: |
|
||||
and
|
||||
(
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
|
||||
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
)
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
@@ -1137,7 +973,7 @@ stages:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
persistCredentials: true
|
||||
fetchDepth: 1
|
||||
fetchDepth: 1
|
||||
- bash: ./docs.sh Windows
|
||||
displayName: Create openapi.json
|
||||
- bash: |
|
||||
@@ -1145,9 +981,10 @@ stages:
|
||||
git config --global user.name "Servarr"
|
||||
git checkout -b api-docs
|
||||
git add .
|
||||
if git status | grep -q modified
|
||||
git status
|
||||
if git status | grep modified
|
||||
then
|
||||
git commit -am 'Automated API Docs update'
|
||||
git commit -am 'Automated API Docs update [skip ci]'
|
||||
git push -f --set-upstream origin api-docs
|
||||
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/readarr/readarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
|
||||
else
|
||||
@@ -1171,53 +1008,68 @@ stages:
|
||||
|
||||
- job: Analyze_Backend
|
||||
displayName: Backend
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
|
||||
variables:
|
||||
disable.coverage.autogenerate: 'true'
|
||||
EnableAnalyzers: 'false'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .net core 2.1'
|
||||
inputs:
|
||||
version: 2.1.815
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core 3.1'
|
||||
inputs:
|
||||
version: 3.1.413
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core 5.0'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@3
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
path: $(nugetCacheFolder)
|
||||
displayName: Cache NuGet packages
|
||||
|
||||
- task: SonarCloudPrepare@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'readarr'
|
||||
scannerMode: 'dotnet'
|
||||
scannerMode: 'MSBuild'
|
||||
projectKey: 'Readarr_Readarr'
|
||||
projectName: 'Readarr'
|
||||
projectVersion: '$(readarrVersion)'
|
||||
extraProperties: |
|
||||
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
||||
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,./src/Libraries/**
|
||||
sonar.coverage.exclusions=**/Readarr.Api.V1/**/*
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
./build.sh --backend -f net6.0 -r linux-x64
|
||||
TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@3
|
||||
env:
|
||||
NUGET_PACKAGES: $(nugetCacheFolder)
|
||||
- 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:
|
||||
@@ -1225,6 +1077,7 @@ stages:
|
||||
- Unit_Test
|
||||
- Integration
|
||||
- Automation
|
||||
- Build_Backend_Other
|
||||
condition: eq(variables['system.pullrequest.isfork'], false)
|
||||
displayName: Build Status Report
|
||||
jobs:
|
||||
@@ -1248,4 +1101,3 @@ stages:
|
||||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
DISCORDTHREADID: $(discordThreadId)
|
||||
|
||||
|
||||
6
build.sh
6
build.sh
@@ -23,7 +23,7 @@ UpdateVersionNumber()
|
||||
echo "Updating Version Info"
|
||||
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props
|
||||
sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props
|
||||
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" distribution/osx/Readarr.app/Contents/Info.plist
|
||||
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" macOS/Readarr.app/Contents/Info.plist
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ PackageMacOSApp()
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
cp -r distribution/osx/Readarr.app $folder
|
||||
cp -r macOS/Readarr.app $folder
|
||||
mkdir -p $folder/Readarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
@@ -245,7 +245,7 @@ BuildInstaller()
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe distribution/windows/setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
|
||||
Binary file not shown.
14
docs.sh
14
docs.sh
@@ -1,7 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
@@ -25,21 +21,15 @@ slnFile=src/Readarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Readarr.Console.dll
|
||||
else
|
||||
application=Readarr.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/Readarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/Readarr.console.dll" v1 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
||||
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,18 +2,16 @@ 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 }],
|
||||
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
|
||||
['@babel/plugin-proposal-optional-chaining', { loose }],
|
||||
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
|
||||
|
||||
// Stage 2
|
||||
'@babel/plugin-transform-export-namespace-from',
|
||||
'@babel/plugin-proposal-export-namespace-from',
|
||||
|
||||
// Stage 3
|
||||
['@babel/plugin-transform-class-properties', { loose }],
|
||||
['@babel/plugin-proposal-class-properties', { loose }],
|
||||
'@babel/plugin-syntax-dynamic-import'
|
||||
],
|
||||
env: {
|
||||
|
||||
@@ -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({
|
||||
@@ -182,7 +181,7 @@ module.exports = (env) => {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: '3.39'
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -203,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]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -165,8 +165,7 @@ function HistoryDetails(props) {
|
||||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message,
|
||||
indexer
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@@ -178,21 +177,11 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
indexer ?
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
message ?
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/> :
|
||||
null
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
@@ -218,12 +218,10 @@ class HistoryRow extends Component {
|
||||
key={name}
|
||||
className={styles.details}
|
||||
>
|
||||
<div className={styles.actionContents}>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -289,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);
|
||||
@@ -306,7 +299,7 @@ class Queue extends Component {
|
||||
return !!(item && item.authorId && item.bookId);
|
||||
})
|
||||
)}
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
@@ -345,8 +338,4 @@ Queue.propTypes = {
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Queue.defaultProps = {
|
||||
count: 0
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
|
||||
@@ -98,7 +98,6 @@ class QueueRow extends Component {
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
downloadForced,
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
@@ -390,7 +389,6 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!author}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -420,7 +418,6 @@ QueueRow.propTypes = {
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
downloadForced: PropTypes.bool.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
|
||||
177
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
177
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
@@ -0,0 +1,177 @@
|
||||
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 = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: 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,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, 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>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</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,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
@@ -1,230 +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 {
|
||||
remove: 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'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
remove: 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;
|
||||
178
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
178
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
@@ -0,0 +1,178 @@
|
||||
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 = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: 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,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, 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>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</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,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
||||
@@ -32,7 +32,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';
|
||||
@@ -247,7 +247,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={Updates}
|
||||
component={UpdatesConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.version {
|
||||
margin: 0 3px;
|
||||
font-weight: bold;
|
||||
font-family: var(--defaultFontFamily);
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
@@ -65,12 +64,12 @@ function AppUpdatedModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AppUpdated', { appName: 'Readarr' })}
|
||||
Readarr Updated
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Readarr', version })} blockClassName={styles.version} />
|
||||
Version <span className={styles.version}>{version}</span> of Readarr has been installed, in order to get the latest changes you'll need to reload Readarr.
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -78,14 +77,16 @@ function AppUpdatedModalContent(props) {
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!update.changes &&
|
||||
<div>
|
||||
<div className={styles.changes}>
|
||||
{translate('WhatsNew')}
|
||||
What's new?
|
||||
</div>
|
||||
|
||||
<UpdateChanges
|
||||
@@ -112,14 +113,14 @@ function AppUpdatedModalContent(props) {
|
||||
<Button
|
||||
onPress={onSeeChangesPress}
|
||||
>
|
||||
{translate('RecentChanges')}
|
||||
Recent Changes
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Reload')}
|
||||
Reload
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -7,7 +7,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ConnectionLostModal.css';
|
||||
|
||||
function ConnectionLostModal(props) {
|
||||
@@ -23,16 +22,16 @@ function ConnectionLostModal(props) {
|
||||
>
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('ConnectionLost')}
|
||||
Connection Lost
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('ConnectionLostToBackend', { appName: 'Readarr' })}
|
||||
Readarr has lost its connection to the backend and will need to be reloaded to restore functionality.
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
{translate('ConnectionLostReconnect', { appName: 'Readarr' })}
|
||||
Readarr will try to connect automatically, or you can click reload below.
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
@@ -40,7 +39,7 @@ function ConnectionLostModal(props) {
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Reload')}
|
||||
Reload
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import AuthorsAppState from './AuthorsAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
@@ -36,24 +33,8 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
authors: AuthorsAppState;
|
||||
commands: CommandAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Author from 'Author/Author';
|
||||
|
||||
interface AuthorsAppState
|
||||
extends AppSectionState<Author>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
|
||||
deleteOptions: {
|
||||
addImportListExclusion: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default AuthorsAppState;
|
||||
@@ -1,6 +0,0 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
||||
@@ -1,23 +1,18 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
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,
|
||||
@@ -32,17 +27,14 @@ export interface NotificationAppState
|
||||
extends AppSectionState<Notification>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
notifications: NotificationAppState;
|
||||
ui: UiSettingsAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
@@ -1,21 +0,0 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export type AuthorStatus = 'continuing' | 'ended';
|
||||
|
||||
interface Author extends ModelBase {
|
||||
added: string;
|
||||
genres: string[];
|
||||
monitored: boolean;
|
||||
overview: string;
|
||||
path: string;
|
||||
qualityProfileId: number;
|
||||
metadataProfileId: number;
|
||||
rootFolderPath: string;
|
||||
sortName: string;
|
||||
status: AuthorStatus;
|
||||
tags: number[];
|
||||
authorName: string;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default Author;
|
||||
@@ -7,10 +7,13 @@ function findImage(images, coverType) {
|
||||
}
|
||||
|
||||
function getUrl(image, coverType, size) {
|
||||
const imageUrl = image?.url;
|
||||
if (image) {
|
||||
// Remove protocol
|
||||
let url = image.url;
|
||||
|
||||
if (imageUrl) {
|
||||
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
|
||||
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
||||
function AuthorNameLink({ titleSlug, authorName, ...otherProps }) {
|
||||
function AuthorNameLink({ titleSlug, authorName }) {
|
||||
const link = `/author/${titleSlug}`;
|
||||
|
||||
return (
|
||||
<Link to={link} {...otherProps}>
|
||||
<Link to={link}>
|
||||
{authorName}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AuthorStatus } from 'Author/Author';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export function getAuthorStatusDetails(status: AuthorStatus) {
|
||||
let statusDetails = {
|
||||
icon: icons.AUTHOR_CONTINUING,
|
||||
title: translate('StatusEndedContinuing'),
|
||||
message: translate('ContinuingMoreBooksAreExpected'),
|
||||
};
|
||||
|
||||
if (status === 'ended') {
|
||||
statusDetails = {
|
||||
icon: icons.AUTHOR_ENDED,
|
||||
title: translate('StatusEndedEnded'),
|
||||
message: translate('ContinuingNoAdditionalBooksAreExpected'),
|
||||
};
|
||||
}
|
||||
|
||||
return statusDetails;
|
||||
}
|
||||
@@ -44,10 +44,6 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.authorNavigationButtons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
@@ -6,7 +6,6 @@ interface CssExports {
|
||||
'authorUpButton': string;
|
||||
'contentContainer': string;
|
||||
'errorMessage': string;
|
||||
'filterIcon': string;
|
||||
'innerContentBody': string;
|
||||
'metadataMessage': string;
|
||||
'selectedTab': string;
|
||||
|
||||
@@ -7,7 +7,6 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||
import Alert from 'Components/Alert';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -18,7 +17,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
@@ -240,14 +239,9 @@ class AuthorDetails extends Component {
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
statistics = {}
|
||||
statistics
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
@@ -413,25 +407,22 @@ class AuthorDetails extends Component {
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !booksError && !bookFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
!isPopulated && !booksError && !bookFilesError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && booksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
!isFetching && booksError &&
|
||||
<div>
|
||||
{translate('LoadingBooksFailed')}
|
||||
</Alert> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && bookFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
!isFetching && bookFilesError &&
|
||||
<div>
|
||||
{translate('LoadingBookFilesFailed')}
|
||||
</Alert> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -444,7 +435,7 @@ class AuthorDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('BooksTotal', [totalBookCount])}
|
||||
{translate('BooksTotal', [statistics.totalBookCount])}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
@@ -472,7 +463,7 @@ class AuthorDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('FilesTotal', [bookFileCount])}
|
||||
{translate('FilesTotal', [statistics.bookFileCount])}
|
||||
</Tab>
|
||||
|
||||
{
|
||||
|
||||
@@ -155,6 +155,7 @@ function createMapStateToProps() {
|
||||
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
|
||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id }));
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||
|
||||
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||
const isRenamingAuthor = (
|
||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||
|
||||
@@ -136,9 +136,8 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import AuthorPoster from 'Author/AuthorPoster';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
@@ -12,7 +11,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
@@ -26,7 +25,12 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
function getFanartUrl(images) {
|
||||
return images.find((x) => x.coverType === 'fanart')?.url;
|
||||
const fanartImage = images.find((x) => x.coverType === 'fanart');
|
||||
|
||||
if (fanartImage) {
|
||||
// Remove protocol
|
||||
return fanartImage.url.replace(/^https?:/, '');
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorDetailsHeader extends Component {
|
||||
@@ -88,11 +92,11 @@ class AuthorDetailsHeader extends Component {
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
||||
|
||||
const continuing = status === 'continuing';
|
||||
|
||||
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
||||
|
||||
if (bookFileCount === 1) {
|
||||
@@ -214,7 +218,7 @@ class AuthorDetailsHeader extends Component {
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileName
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
@@ -237,16 +241,16 @@ class AuthorDetailsHeader extends Component {
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={statusDetails.message}
|
||||
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={statusDetails.icon}
|
||||
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{statusDetails.title}
|
||||
{continuing ? 'Continuing' : 'Deceased'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -27,9 +27,3 @@
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.indexerFlags {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
1
frontend/src/Author/Details/BookRow.css.d.ts
vendored
1
frontend/src/Author/Details/BookRow.css.d.ts
vendored
@@ -1,7 +1,6 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'indexerFlags': string;
|
||||
'monitored': string;
|
||||
'pageCount': string;
|
||||
'position': string;
|
||||
|
||||
@@ -2,17 +2,12 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
|
||||
import BookTitleLink from 'Book/BookTitleLink';
|
||||
import IndexerFlags from 'Book/IndexerFlags';
|
||||
import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import StarRating from 'Components/StarRating';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookStatus from './BookStatus';
|
||||
import styles from './BookRow.css';
|
||||
|
||||
@@ -64,7 +59,6 @@ class BookRow extends Component {
|
||||
releaseDate,
|
||||
title,
|
||||
seriesTitle,
|
||||
authorName,
|
||||
position,
|
||||
pageCount,
|
||||
ratings,
|
||||
@@ -72,7 +66,6 @@ class BookRow extends Component {
|
||||
authorMonitored,
|
||||
titleSlug,
|
||||
bookFiles,
|
||||
indexerFlags,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
@@ -196,24 +189,6 @@ class BookRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
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 === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -236,7 +211,6 @@ class BookRow extends Component {
|
||||
bookId={id}
|
||||
authorId={authorId}
|
||||
bookTitle={title}
|
||||
authorName={authorName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -255,11 +229,9 @@ BookRow.propTypes = {
|
||||
releaseDate: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
seriesTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
position: PropTypes.string,
|
||||
pageCount: PropTypes.number,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
authorMonitored: PropTypes.bool.isRequired,
|
||||
@@ -271,8 +243,4 @@ BookRow.propTypes = {
|
||||
onMonitorBookPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
BookRow.defaultProps = {
|
||||
indexerFlags: 0
|
||||
};
|
||||
|
||||
export default BookRow;
|
||||
|
||||
@@ -7,18 +7,21 @@ import BookRow from './BookRow';
|
||||
const selectBookFiles = createSelector(
|
||||
(state) => state.bookFiles,
|
||||
(bookFiles) => {
|
||||
const { items } = bookFiles;
|
||||
const {
|
||||
items
|
||||
} = bookFiles;
|
||||
|
||||
return items.reduce((acc, file) => {
|
||||
const bookFileDict = items.reduce((acc, file) => {
|
||||
const bookId = file.bookId;
|
||||
if (!acc.hasOwnProperty(bookId)) {
|
||||
acc[bookId] = [];
|
||||
}
|
||||
|
||||
acc[bookId].push(file);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return bookFileDict;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -28,14 +31,9 @@ function createMapStateToProps() {
|
||||
selectBookFiles,
|
||||
(state, { id }) => id,
|
||||
(author = {}, bookFiles, bookId) => {
|
||||
const files = bookFiles[bookId] ?? [];
|
||||
const bookFile = files[0];
|
||||
|
||||
return {
|
||||
authorMonitored: author.monitored,
|
||||
authorName: author.authorName,
|
||||
bookFiles: files,
|
||||
indexerFlags: bookFile ? bookFile.indexerFlags : 0
|
||||
bookFiles: bookFiles[bookId] ?? []
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -173,7 +173,7 @@ class AuthorEditorFooter extends Component {
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'monitored', value: translate('Monitored') },
|
||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||
];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
|
||||
import AuthorHistoryModalContent from './AuthorHistoryModalContent';
|
||||
|
||||
@@ -15,7 +14,6 @@ function AuthorHistoryModal(props) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthorHistoryContentConnector
|
||||
|
||||
@@ -5,7 +5,6 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorHistoryTableContent from './AuthorHistoryTableContent';
|
||||
|
||||
class AuthorHistoryModalContent extends Component {
|
||||
@@ -21,7 +20,7 @@ class AuthorHistoryModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('History')}
|
||||
History
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -32,7 +31,7 @@ class AuthorHistoryModalContent extends Component {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.details,
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'details': string;
|
||||
'sourceTitle': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import BookFormats from 'Book/BookFormats';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
@@ -12,7 +11,6 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthorHistoryRow.css';
|
||||
|
||||
@@ -77,8 +75,6 @@ class AuthorHistoryRow extends Component {
|
||||
sourceTitle,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
date,
|
||||
data,
|
||||
book
|
||||
@@ -110,19 +106,11 @@ class AuthorHistoryRow extends Component {
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<BookFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={date}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<TableRowCell className={styles.details}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
@@ -139,13 +127,14 @@ class AuthorHistoryRow extends Component {
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
@@ -171,8 +160,6 @@ AuthorHistoryRow.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
fullAuthor: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.container {
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'container': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
|
||||
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
|
||||
import styles from './AuthorHistoryTable.css';
|
||||
|
||||
function AuthorHistoryTable(props) {
|
||||
const {
|
||||
@@ -9,12 +8,10 @@ function AuthorHistoryTable(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<AuthorHistoryContentConnector
|
||||
component={AuthorHistoryTableContent}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
<AuthorHistoryContentConnector
|
||||
component={AuthorHistoryTableContent}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.blankpad {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
|
||||
import styles from './AuthorHistoryTableContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -17,41 +15,32 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'book',
|
||||
label: () => translate('Book'),
|
||||
label: 'Book',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate( 'SourceTitle'),
|
||||
label: 'Source Title',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
label: 'Quality',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
label: 'Date',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
label: 'Details',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: 'Actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
@@ -75,7 +64,7 @@ class AuthorHistoryTableContent extends Component {
|
||||
const hasItems = !!items.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
@@ -90,7 +79,7 @@ class AuthorHistoryTableContent extends Component {
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div className={styles.blankpad}>
|
||||
<div>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
}
|
||||
@@ -114,7 +103,7 @@ class AuthorHistoryTableContent extends Component {
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createAuthorClientSideCollectionItemsSelector('authorIndex'),
|
||||
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
@@ -24,17 +24,17 @@ function createMapStateToProps() {
|
||||
(
|
||||
author,
|
||||
isRefreshingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...author,
|
||||
isRefreshingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class AuthorIndexOverview extends Component {
|
||||
status,
|
||||
titleSlug,
|
||||
nextAiring,
|
||||
statistics = {},
|
||||
statistics,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
@@ -113,11 +113,10 @@ class AuthorIndexOverview extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
bookCount,
|
||||
sizeOnDisk,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -180,7 +179,6 @@ class AuthorIndexOverview extends Component {
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
bookCount={bookCount}
|
||||
availableBookCount={availableBookCount}
|
||||
bookFileCount={bookFileCount}
|
||||
totalBookCount={totalBookCount}
|
||||
posterWidth={posterWidth}
|
||||
|
||||
@@ -5,7 +5,6 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
||||
import styles from './AuthorIndexOverviewInfo.css';
|
||||
|
||||
@@ -77,9 +76,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: translate('QualityProfile'),
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ class AuthorIndexPoster extends Component {
|
||||
titleSlug,
|
||||
status,
|
||||
nextAiring,
|
||||
statistics = {},
|
||||
statistics,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
@@ -110,11 +110,10 @@ class AuthorIndexPoster extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
bookCount,
|
||||
sizeOnDisk,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -214,7 +213,6 @@ class AuthorIndexPoster extends Component {
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
bookCount={bookCount}
|
||||
availableBookCount={availableBookCount}
|
||||
bookFileCount={bookFileCount}
|
||||
totalBookCount={totalBookCount}
|
||||
posterWidth={posterWidth}
|
||||
@@ -235,12 +233,12 @@ class AuthorIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -11,15 +11,14 @@ function AuthorIndexProgressBar(props) {
|
||||
monitored,
|
||||
status,
|
||||
bookCount,
|
||||
availableBookCount,
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
posterWidth,
|
||||
detailedProgressBar
|
||||
} = props;
|
||||
|
||||
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||
const text = `${availableBookCount} / ${bookCount}`;
|
||||
const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
|
||||
const text = `${bookCount} / ${totalBookCount}`;
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
@@ -30,7 +29,7 @@ function AuthorIndexProgressBar(props) {
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
showText={detailedProgressBar}
|
||||
text={text}
|
||||
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
width={posterWidth}
|
||||
/>
|
||||
);
|
||||
@@ -40,7 +39,6 @@ AuthorIndexProgressBar.propTypes = {
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
bookCount: PropTypes.number.isRequired,
|
||||
availableBookCount: PropTypes.number.isRequired,
|
||||
bookFileCount: PropTypes.number.isRequired,
|
||||
totalBookCount: PropTypes.number.isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
|
||||
@@ -90,7 +90,7 @@ class AuthorIndexRow extends Component {
|
||||
nextBook,
|
||||
lastBook,
|
||||
added,
|
||||
statistics = {},
|
||||
statistics,
|
||||
genres,
|
||||
ratings,
|
||||
path,
|
||||
@@ -110,11 +110,10 @@ class AuthorIndexRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
bookCount,
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -209,7 +208,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile?.name ?? ''}
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -220,7 +219,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{metadataProfile?.name ?? ''}
|
||||
{metadataProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -287,7 +286,7 @@ class AuthorIndexRow extends Component {
|
||||
}
|
||||
|
||||
if (name === 'bookProgress') {
|
||||
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||
const progress = bookCount ? bookFileCount / bookCount * 100 : 100;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
@@ -298,8 +297,8 @@ class AuthorIndexRow extends Component {
|
||||
progress={progress}
|
||||
kind={getProgressBarKind(status, monitored, progress)}
|
||||
showText={true}
|
||||
text={`${availableBookCount} / ${bookCount}`}
|
||||
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||
text={`${bookCount} / ${totalBookCount}`}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
width={125}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import Icon from 'Components/Icon';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -16,8 +15,6 @@ function AuthorStatusCell(props) {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
@@ -31,8 +28,8 @@ function AuthorStatusCell(props) {
|
||||
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={statusDetails.icon}
|
||||
title={`${statusDetails.title}: ${statusDetails.message}`}
|
||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './NoAuthor.css';
|
||||
|
||||
function NoAuthor(props) {
|
||||
@@ -32,7 +31,7 @@ function NoAuthor(props) {
|
||||
to="/settings/mediamanagement"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
{translate('AddRootFolder')}
|
||||
Add Root Folder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +40,7 @@ function NoAuthor(props) {
|
||||
to="/add/search"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
{translate('AddNewAuthor')}
|
||||
Add New Author
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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, isMonitored, isCutoffNotMet) {
|
||||
const revision = quality.revision;
|
||||
@@ -29,36 +28,6 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||
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 BookQuality(props) {
|
||||
const {
|
||||
className,
|
||||
@@ -66,8 +35,7 @@ function BookQuality(props) {
|
||||
quality,
|
||||
size,
|
||||
isMonitored,
|
||||
isCutoffNotMet,
|
||||
showRevision
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
@@ -82,15 +50,13 @@ function BookQuality(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Label
|
||||
className={className}
|
||||
kind={kind}
|
||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>{revisionLabel(className, quality, showRevision)}
|
||||
</span>
|
||||
<Label
|
||||
className={className}
|
||||
kind={kind}
|
||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,14 +66,12 @@ BookQuality.propTypes = {
|
||||
quality: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
isMonitored: PropTypes.bool,
|
||||
isCutoffNotMet: PropTypes.bool,
|
||||
showRevision: PropTypes.bool
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
};
|
||||
|
||||
BookQuality.defaultProps = {
|
||||
title: '',
|
||||
isMonitored: true,
|
||||
showRevision: false
|
||||
isMonitored: true
|
||||
};
|
||||
|
||||
export default BookQuality;
|
||||
|
||||
@@ -38,7 +38,6 @@ class BookSearchCell extends Component {
|
||||
const {
|
||||
bookId,
|
||||
bookTitle,
|
||||
authorName,
|
||||
isSearching,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
@@ -61,7 +60,6 @@ class BookSearchCell extends Component {
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
bookId={bookId}
|
||||
bookTitle={bookTitle}
|
||||
authorName={authorName}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
{...otherProps}
|
||||
/>
|
||||
@@ -75,7 +73,6 @@ BookSearchCell.propTypes = {
|
||||
bookId: PropTypes.number.isRequired,
|
||||
authorId: PropTypes.number.isRequired,
|
||||
bookTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -99,14 +99,9 @@ class BookDetails extends Component {
|
||||
nextBook,
|
||||
isSearching,
|
||||
onRefreshPress,
|
||||
onSearchPress,
|
||||
statistics = {}
|
||||
onSearchPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
@@ -243,21 +238,21 @@ class BookDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('History')}
|
||||
History
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Search')}
|
||||
Search
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('FilesTotal', [bookFileCount])}
|
||||
Files
|
||||
</Tab>
|
||||
|
||||
{
|
||||
@@ -340,7 +335,6 @@ BookDetails.propTypes = {
|
||||
ratings: PropTypes.object.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -69,21 +69,16 @@ function createMapStateToProps() {
|
||||
|
||||
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
|
||||
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
|
||||
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||
const isRefreshing = (
|
||||
isCommandExecuting(isRefreshingCommand) &&
|
||||
isRefreshingCommand.body.bookId === book.id
|
||||
);
|
||||
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
|
||||
const isSearching = (
|
||||
isCommandExecuting(isSearchingCommand) &&
|
||||
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
||||
);
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||
const isRenamingAuthor = (
|
||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||
isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1
|
||||
|
||||
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||
const isRefreshing = (
|
||||
isCommandExecuting(isRefreshingCommand) &&
|
||||
isRefreshingCommand.body.bookId === book.id
|
||||
);
|
||||
|
||||
const isFetching = isBookFilesFetching || editions.isFetching;
|
||||
@@ -95,8 +90,6 @@ function createMapStateToProps() {
|
||||
author,
|
||||
isRefreshing,
|
||||
isSearching,
|
||||
isRenamingFiles,
|
||||
isRenamingAuthor,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
bookFilesError,
|
||||
@@ -132,27 +125,9 @@ class BookDetailsConnector extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
anyReleaseOk,
|
||||
isRenamingFiles,
|
||||
isRenamingAuthor
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||
(prevProps.isRenamingAuthor && !isRenamingAuthor) ||
|
||||
if (prevProps.id !== this.props.id ||
|
||||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && anyReleaseOk === true)
|
||||
) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
|
||||
// If the id has changed we need to clear the book
|
||||
// files and fetch from the server.
|
||||
|
||||
if (prevProps.id !== id) {
|
||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
@@ -222,8 +197,6 @@ class BookDetailsConnector extends Component {
|
||||
BookDetailsConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
anyReleaseOk: PropTypes.bool,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||
isBookFetching: PropTypes.bool,
|
||||
isBookPopulated: PropTypes.bool,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
|
||||
@@ -84,15 +84,9 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.authorLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-right: 15px;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.duration {
|
||||
margin-right: 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.detailsLabel {
|
||||
@@ -123,9 +117,8 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'alternateTitlesIconContainer': string;
|
||||
'authorLink': string;
|
||||
'backdrop': string;
|
||||
'backdropOverlay': string;
|
||||
'cover': string;
|
||||
|
||||
@@ -2,7 +2,6 @@ import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import BookCover from 'Book/BookCover';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -22,7 +21,12 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
function getFanartUrl(images) {
|
||||
return images.find((x) => x.coverType === 'fanart')?.url;
|
||||
const fanartImage = images.find((x) => x.coverType === 'fanart');
|
||||
|
||||
if (fanartImage) {
|
||||
// Remove protocol
|
||||
return fanartImage.url.replace(/^https?:/, '');
|
||||
}
|
||||
}
|
||||
|
||||
class BookDetailsHeader extends Component {
|
||||
@@ -114,7 +118,7 @@ class BookDetailsHeader extends Component {
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={isSmallScreen ? 30 : 40}
|
||||
size={isSmallScreen ? 30: 40}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
@@ -132,12 +136,7 @@ class BookDetailsHeader extends Component {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AuthorNameLink
|
||||
className={styles.authorLink}
|
||||
titleSlug={author.titleSlug}
|
||||
authorName={author.authorName}
|
||||
/>
|
||||
|
||||
{author.authorName}
|
||||
{
|
||||
!!pageCount &&
|
||||
<span className={styles.duration}>
|
||||
|
||||
@@ -89,7 +89,7 @@ class BookEditorFooter extends Component {
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'monitored', value: translate('Monitored') },
|
||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||
];
|
||||
|
||||
@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createBookClientSideCollectionItemsSelector('bookIndex'),
|
||||
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_BOOK),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
|
||||
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
|
||||
|
||||
@@ -5,7 +5,6 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
||||
import styles from './BookIndexOverviewInfo.css';
|
||||
|
||||
@@ -72,9 +71,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: translate('QualityProfile'),
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -16,8 +16,8 @@ function BookIndexProgressBar(props) {
|
||||
detailedProgressBar
|
||||
} = props;
|
||||
|
||||
const progress = bookFileCount && bookCount ? (totalBookCount / bookCount) * 100 : 0;
|
||||
const text = `${bookFileCount ? bookCount : 0} / ${totalBookCount}`;
|
||||
const progress = bookCount ? bookFileCount / totalBookCount * 100 : 0;
|
||||
const text = `${bookFileCount} / ${bookCount}`;
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
@@ -28,11 +28,7 @@ function BookIndexProgressBar(props) {
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
showText={detailedProgressBar}
|
||||
text={text}
|
||||
title={translate('BookProgressBarText', {
|
||||
bookCount: bookFileCount ? bookCount : 0,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
})}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
width={posterWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile?.name ?? ''}
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -229,6 +229,7 @@ class BookIndexRow extends Component {
|
||||
className={styles[name]}
|
||||
>
|
||||
{bookFileCount}
|
||||
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -9,21 +9,19 @@ function BookInteractiveSearchModal(props) {
|
||||
isOpen,
|
||||
bookId,
|
||||
bookTitle,
|
||||
authorName,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<BookInteractiveSearchModalContent
|
||||
bookId={bookId}
|
||||
bookTitle={bookTitle}
|
||||
authorName={authorName}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
@@ -34,7 +32,6 @@ BookInteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
bookId: PropTypes.number.isRequired,
|
||||
bookTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -7,23 +7,18 @@ 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 BookInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
bookId,
|
||||
bookTitle,
|
||||
authorName,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{bookId === null ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderBookAuthor', { bookTitle, authorName })
|
||||
}
|
||||
Interactive Search {bookId != null && `- ${bookTitle}`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
@@ -37,7 +32,7 @@ function BookInteractiveSearchModalContent(props) {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
@@ -47,7 +42,6 @@ function BookInteractiveSearchModalContent(props) {
|
||||
BookInteractiveSearchModalContent.propTypes = {
|
||||
bookId: PropTypes.number.isRequired,
|
||||
bookTitle: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.container {
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'container': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
|
||||
import styles from './BookFileEditorTable.css';
|
||||
|
||||
function BookFileEditorTable(props) {
|
||||
const {
|
||||
@@ -8,11 +7,9 @@ function BookFileEditorTable(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<BookFileEditorTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
<BookFileEditorTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.filesTable {
|
||||
margin: 10px;
|
||||
padding-top: 5px;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 15px;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-top: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
@@ -13,15 +13,9 @@
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.selectInput {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.blankpad {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'blankpad': string;
|
||||
'filesTable': string;
|
||||
'selectInput': string;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -116,12 +115,12 @@ class BookFileEditorTableContent extends Component {
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]);
|
||||
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
|
||||
|
||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{
|
||||
isFetching && !isPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
@@ -130,13 +129,13 @@ class BookFileEditorTableContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<Alert kind={kinds.DANGER}>{error}</Alert> :
|
||||
<div>{error}</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length ?
|
||||
<div className={styles.blankpad}>
|
||||
<div>
|
||||
No book files to manage.
|
||||
</div> :
|
||||
null
|
||||
@@ -174,30 +173,26 @@ class BookFileEditorTableContent extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && items.length ? (
|
||||
<div className={styles.actions}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onPress={this.onDeletePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
<div className={styles.actions}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onPress={this.onDeletePress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton>
|
||||
|
||||
<div className={styles.selectInput}>
|
||||
<SelectInput
|
||||
name="quality"
|
||||
value="selectQuality"
|
||||
values={qualityOptions}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
<div className={styles.selectInput}>
|
||||
<SelectInput
|
||||
name="quality"
|
||||
value="selectQuality"
|
||||
values={qualityOptions}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
@@ -208,7 +203,7 @@ class BookFileEditorTableContent extends Component {
|
||||
onConfirm={this.onConfirmDelete}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,15 +27,14 @@ class BookshelfBook extends Component {
|
||||
title,
|
||||
disambiguation,
|
||||
monitored,
|
||||
statistics = {},
|
||||
statistics,
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
percentOfBooks = 0
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
percentOfBooks
|
||||
} = statistics;
|
||||
|
||||
return (
|
||||
@@ -60,14 +59,10 @@ class BookshelfBook extends Component {
|
||||
percentOfBooks < 100 && monitored && styles.missingWanted,
|
||||
percentOfBooks === 100 && styles.allBooks
|
||||
)}
|
||||
title={translate('BookProgressBarText', {
|
||||
bookCount: bookFileCount ? bookCount : 0,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
})}
|
||||
title={translate('BookFileCounttotalBookCountBooksDownloadedInterp', [bookFileCount, totalBookCount])}
|
||||
>
|
||||
{
|
||||
totalBookCount === 0 ? '0/0' : `${bookFileCount ? bookCount : 0}/${totalBookCount}`
|
||||
totalBookCount === 0 ? '0/0' : `${bookFileCount}/${totalBookCount}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ class BookshelfFooter extends Component {
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'monitored', value: translate('Monitored') },
|
||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||
];
|
||||
@@ -143,7 +143,7 @@ class BookshelfFooter extends Component {
|
||||
|
||||
<div>
|
||||
<div className={styles.label}>
|
||||
{translate('CountAuthorsSelected', { selectedCount })}
|
||||
{selectedCount} Author(s) Selected
|
||||
</div>
|
||||
|
||||
<SpinnerButton
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookshelfBook from './BookshelfBook';
|
||||
import styles from './BookshelfRow.css';
|
||||
|
||||
@@ -29,8 +30,6 @@ class BookshelfRow extends Component {
|
||||
onBookMonitoredPress
|
||||
} = this.props;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
@@ -53,8 +52,8 @@ class BookshelfRow extends Component {
|
||||
<VirtualTableRowCell className={styles.status}>
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={statusDetails.icon}
|
||||
title={statusDetails.title}
|
||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class CalendarConnector extends Component {
|
||||
gotoCalendarToday
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']);
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchCalendar();
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export interface CommandBody {
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
completionMessage: string;
|
||||
requiresDiskAccess: boolean;
|
||||
isExclusive: boolean;
|
||||
isLongRunning: boolean;
|
||||
name: string;
|
||||
lastExecutionTime: string;
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
authorId?: number;
|
||||
authorIds?: number[];
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
name: string;
|
||||
commandName: string;
|
||||
message: string;
|
||||
body: CommandBody;
|
||||
priority: string;
|
||||
status: string;
|
||||
result: string;
|
||||
queued: string;
|
||||
started: string;
|
||||
ended: string;
|
||||
duration: string;
|
||||
trigger: string;
|
||||
stateChangeTime: string;
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
lastExecutionTime: string;
|
||||
}
|
||||
|
||||
export default Command;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user