Compare commits

..

1 Commits

Author SHA1 Message Date
Qstick
edc522e412 Remove Non-Failing Rules
(cherry picked from commit e8b862a38059da4fcb5ab1ab51cf838fe5424f47)
2023-03-16 00:33:19 +00:00
1654 changed files with 17745 additions and 42859 deletions

View File

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

View File

@@ -1,19 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "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"]
}
}
}

View File

@@ -36,18 +36,9 @@ dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _ dotnet_naming_style.instance_field_style.required_prefix = _
# Prefer "var" everywhere # Prefer "var" everywhere
csharp_style_var_for_built_in_types = true csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true csharp_style_var_elsewhere = true:suggestion
# Prefer "out" variables to be declared inline
csharp_style_inlined_variable_declaration = true
# Using directive is unnecessary.
dotnet_diagnostic.IDE0005.severity = error
# Use var instead of explicit type
dotnet_diagnostic.IDE0007.severity = error
# Inline variable declaration
dotnet_diagnostic.IDE0018.severity = error
# Stylecop Rules # Stylecop Rules
dotnet_diagnostic.SA0001.severity = none dotnet_diagnostic.SA0001.severity = none
@@ -213,9 +204,6 @@ dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.severity = suggestion dotnet_diagnostic.CA2008.severity = suggestion
dotnet_diagnostic.CA2009.severity = suggestion
dotnet_diagnostic.CA2010.severity = suggestion
dotnet_diagnostic.CA2011.severity = suggestion
dotnet_diagnostic.CA2012.severity = suggestion dotnet_diagnostic.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion dotnet_diagnostic.CA2100.severity = suggestion
@@ -275,7 +263,7 @@ dotnet_diagnostic.CA5397.severity = suggestion
[*.{js,html,hbs,less,css,ts,tsx}] [*.{js,html,js,hbs,less,css}]
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true

9
.esprintrc Normal file
View File

@@ -0,0 +1,9 @@
{
"paths": [
"frontend/src/**/*.js"
],
"ignored": [
"**/node_modules/**/*"
],
"port": 5004
}

3
.gitattributes vendored
View File

@@ -3,7 +3,8 @@
# Explicitly set bash scripts to have unix endings # Explicitly set bash scripts to have unix endings
*.sh text eol=lf *.sh text eol=lf
distribution/osx/Readarr text eol=lf distribution/debian/* text eol=lf
macOS/Readarr text eol=lf
# Custom for Visual Studio # Custom for Visual Studio
*.cs diff=csharp *.cs diff=csharp

View File

@@ -1,5 +1,5 @@
name: Bug Report 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'] labels: ['Type: Bug', 'Status: Needs Triage']
body: body:
- type: checkboxes - type: checkboxes
@@ -73,10 +73,3 @@ body:
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering! Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations: validations:
required: true required: true
- type: checkboxes
attributes:
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
options:
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
required: true

View File

@@ -3,3 +3,6 @@ contact_links:
- name: Support via Discord - name: Support via Discord
url: https://readarr.com/discord url: https://readarr.com/discord
about: Chat with users and devs on support and setup related topics. 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.

View File

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

View File

@@ -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).

28
.github/labeler.yml vendored
View File

@@ -1,28 +0,0 @@
'Area: API':
- src/Readarr.Api.V1/**/*
'Area: Db-migration':
- src/NzbDrone.Core/Datastore/Migration/*
'Area: Download Clients':
- src/NzbDrone.Core/Download/Clients/**/*
'Area: Import Lists':
- src/NzbDrone.Core/ImportLists/**/*
'Area: Indexer':
- src/NzbDrone.Core/Indexers/**/*
'Area: Notifications':
- src/NzbDrone.Core/Notifications/**/*
'Area: Organizer':
- src/NzbDrone.Core/Organizer/**/*
'Area: Parser':
- src/NzbDrone.Core/Parser/**/*
'Area: UI':
- frontend/**/*
- package.json
- yarn.lock

View File

@@ -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'

View File

@@ -1,12 +0,0 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4

View File

@@ -9,13 +9,13 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v4 - uses: dessant/lock-threads@v2
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '90' issue-lock-inactive-days: '90'
exclude-issue-created-before: '' issue-exclude-created-before: ''
exclude-any-issue-labels: '' issue-exclude-labels: ''
add-issue-labels: '' issue-lock-labels: ''
issue-comment: '' issue-lock-comment: ''
issue-lock-reason: 'resolved' issue-lock-reason: 'resolved'
process-only: '' process-only: ''

21
.github/workflows/support.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v2
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

5
.gitignore vendored
View File

@@ -120,13 +120,11 @@ _artifacts
_rawPackage/ _rawPackage/
_dotTrace* _dotTrace*
_tests/ _tests/
_temp*
*.Result.xml *.Result.xml
coverage*.xml coverage*.xml
coverage*.json coverage*.json
setup/Output/ setup/Output/
*.~is *.~is
.mono
# .NET Core # .NET Core
project.lock.json project.lock.json
@@ -150,6 +148,3 @@ _temp_*/**/*
## Merge any idea folder ## Merge any idea folder
*/**/.idea */**/.idea
*.iml *.iml
# API doc generation
.config/

View File

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

26
.vscode/launch.json vendored
View File

@@ -1,26 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": "Run 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
View File

@@ -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

View File

@@ -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 # Readarr
[![Build Status](https://dev.azure.com/Readarr/Readarr/_apis/build/status/Readarr.Readarr?branchName=develop)](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop) [![Build Status](https://dev.azure.com/Readarr/Readarr/_apis/build/status/Readarr.Readarr?branchName=develop)](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
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://readarr.com/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://readarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/readarr)
Note: GitHub Issues are for Bugs and Feature Requests Only Note: GitHub Issues are for Bugs and Feature Requests Only

View File

@@ -9,28 +9,23 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.19' majorVersion: '0.1.5'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.427' dotnetVersion: '6.0.302'
nodeVersion: '20.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-22.04' linuxImage: 'ubuntu-20.04'
macImage: 'macOS-13' macImage: 'macOS-11'
trigger: trigger:
branches: branches:
include: include:
- develop - develop
- master - master
paths:
exclude:
- .github
- src/Readarr.Api.*/openapi.json
pr: pr:
branches: branches:
@@ -38,37 +33,82 @@ pr:
- develop - develop
paths: paths:
exclude: exclude:
- .github
- src/NzbDrone.Core/Localization/Core - src/NzbDrone.Core/Localization/Core
- src/Readarr.Api.*/openapi.json
stages: stages:
- stage: Setup
displayName: Setup - stage: Build_Backend_Windows
displayName: Build Backend
dependsOn: []
jobs: jobs:
- job: - job: Backend
displayName: Build Variables strategy:
matrix:
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: $(imageName)
variables:
# Disable stylecop here - linting errors get caught by the analyze task
EnableAnalyzers: $(enableAnalysis)
steps: steps:
# Set the build name properly. The 'name' property won't recursively expand so hack here: # Set the build name properly. The 'name' property won't recursively expand so hack here:
- bash: echo "##vso[build.updatebuildnumber]$READARRVERSION" - bash: echo "##vso[build.updatebuildnumber]$READARRVERSION"
displayName: Set Build Name displayName: Set Build Name
- checkout: self
submodules: true
fetchDepth: 1
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- bash: | - bash: |
if [[ $BUILD_REASON == "PullRequest" ]]; then SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)" BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
echo $? > not_backend_update
else if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo 0 > not_backend_update sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi fi
cat not_backend_update displayName: Extra Platform Support
displayName: Check for Backend File Changes - task: Cache@2
- publish: not_backend_update inputs:
artifact: not_backend_update key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
displayName: Publish update type path: $(nugetCacheFolder)
- stage: Build_Backend displayName: Cache NuGet packages
displayName: Build Backend - bash: ./build.sh --backend --enable-bsd
dependsOn: Setup 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: jobs:
- job: Backend - job: Backend
strategy: strategy:
@@ -81,10 +121,6 @@ stages:
osName: 'Mac' osName: 'Mac'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
enableAnalysis: 'false' enableAnalysis: 'false'
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
@@ -100,17 +136,22 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
echo $BUNDLEDVERSIONS BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled" if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi 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 - bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Readarr Backend displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \; find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
@@ -118,38 +159,10 @@ stages:
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \; find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows')) 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 - stage: Build_Frontend
displayName: Frontend displayName: Frontend
dependsOn: Setup dependsOn: []
jobs: jobs:
- job: Build - job: Build
strategy: strategy:
@@ -166,10 +179,10 @@ stages:
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseNode@1 - task: NodeTool@0
displayName: Set Node.js version displayName: Set Node.js version
inputs: inputs:
version: $(nodeVersion) versionSpec: '12.x'
- checkout: self - checkout: self
submodules: true submodules: true
fetchDepth: 1 fetchDepth: 1
@@ -178,6 +191,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --frontend - bash: ./build.sh --frontend
@@ -189,10 +203,10 @@ stages:
artifact: '$(osName)Frontend' artifact: '$(osName)Frontend'
displayName: Publish Frontend displayName: Publish Frontend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Installer - stage: Installer
dependsOn: dependsOn:
- Build_Backend - Build_Backend_Windows
- Build_Frontend - Build_Frontend
jobs: jobs:
- job: Windows_Installer - job: Windows_Installer
@@ -216,8 +230,8 @@ stages:
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: | - bash: |
./build.sh --packages --installer ./build.sh --packages --installer
cp distribution/windows/setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe cp 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-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create Installers displayName: Create Installers
- publish: $(Build.ArtifactStagingDirectory) - publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller' artifact: 'WindowsInstaller'
@@ -225,7 +239,7 @@ stages:
- stage: Packages - stage: Packages
dependsOn: dependsOn:
- Build_Backend - Build_Backend_Windows
- Build_Frontend - Build_Frontend
jobs: jobs:
- job: Other_Packages - job: Other_Packages
@@ -367,7 +381,7 @@ stages:
- bash: | - bash: |
echo "Uploading source maps to sentry" echo "Uploading source maps to sentry"
curl -sL https://sentry.io/get-cli/ | bash curl -sL https://sentry.io/get-cli/ | bash
RELEASENAME="Readarr@${READARRVERSION}-${BUILD_SOURCEBRANCHNAME}" RELEASENAME="${READARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
sentry-cli releases new --finalize -p readarr -p readarr-ui -p readarr-update "${RELEASENAME}" sentry-cli releases new --finalize -p readarr -p readarr-ui -p readarr-update "${RELEASENAME}"
sentry-cli releases -p readarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite sentry-cli releases -p readarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
sentry-cli releases set-commits --auto "${RELEASENAME}" sentry-cli releases set-commits --auto "${RELEASENAME}"
@@ -391,29 +405,14 @@ stages:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr) SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg) SENTRY_ORG: $(sentryOrg)
SENTRY_URL: $(sentryUrl) SENTRY_URL: $(sentryUrl)
- stage: Unit_Test - stage: Unit_Test
displayName: Unit Tests displayName: Unit Tests
dependsOn: Build_Backend dependsOn: Build_Backend_Windows
condition: succeeded()
jobs: 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 - job: Unit
displayName: Unit Native displayName: Unit Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace: workspace:
clean: all clean: all
@@ -479,8 +478,6 @@ stages:
- job: Unit_Docker - job: Unit_Docker
displayName: Unit Docker displayName: Unit Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
alpine: alpine:
@@ -494,11 +491,11 @@ stages:
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
timeoutInMinutes: 10 timeoutInMinutes: 10
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .NET'
@@ -532,14 +529,12 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres14 - job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres14 Database displayName: Unit Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests artifactName: LinuxCoreTests
Readarr__Postgres__Host: 'localhost' Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432' Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr' Readarr__Postgres__User: 'readarr'
@@ -549,7 +544,7 @@ stages:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10 timeoutInMinutes: 10
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -560,7 +555,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: $(artifactName) artifactName: 'linux-x64-Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \; - bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable displayName: Make Test Dummy Executable
@@ -583,84 +578,15 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres14 Unit Tests' testRunTitle: 'LinuxCore Postgres 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'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- stage: Integration - stage: Integration
displayName: Integration displayName: Integration
dependsOn: Packages dependsOn: Packages
jobs: 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 - job: Integration_Native
displayName: Integration Native displayName: Integration Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
MacCore: MacCore:
@@ -681,7 +607,7 @@ stages:
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -703,7 +629,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -722,10 +648,8 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres14 - job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres14 Database displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost' Readarr__Postgres__Host: 'localhost'
@@ -757,7 +681,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -780,77 +704,12 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests' testRunTitle: 'Integration LinuxCore Postgres 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'
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_FreeBSD - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace: workspace:
clean: all clean: all
variables: variables:
@@ -895,8 +754,6 @@ stages:
- job: Integration_Docker - job: Integration_Docker
displayName: Integration Docker displayName: Integration Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
alpine: alpine:
@@ -915,7 +772,7 @@ stages:
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
timeoutInMinutes: 15 timeoutInMinutes: 15
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .NET'
@@ -943,7 +800,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -965,7 +822,7 @@ stages:
- stage: Automation - stage: Automation
displayName: Automation displayName: Automation
dependsOn: Packages dependsOn: Packages
jobs: jobs:
- job: Automation - job: Automation
strategy: strategy:
@@ -975,23 +832,20 @@ stages:
artifactName: 'linux-x64' artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64' artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64' artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
failBuild: true
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -1013,7 +867,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -1033,35 +887,20 @@ stages:
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots' TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots - publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots' artifact: '$(osName)AutomationScreenshots'
displayName: Publish Screenshot Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1')) condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
displayName: Publish Screenshot Bundle
- task: PublishTestResults@2 - task: PublishTestResults@2
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(osName) Automation Tests' testRunTitle: '$(osName) Automation Tests'
failTaskOnFailedTests: $(failBuild) failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- stage: Analyze - stage: Analyze
dependsOn: dependsOn: []
- Setup
displayName: Analyze displayName: Analyze
jobs: 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 - job: Lint_Frontend
displayName: Lint Frontend displayName: Lint Frontend
strategy: strategy:
@@ -1075,10 +914,10 @@ stages:
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseNode@1 - task: NodeTool@0
displayName: Set Node.js version displayName: Set Node.js version
inputs: inputs:
version: $(nodeVersion) versionSpec: '12.x'
- checkout: self - checkout: self
submodules: true submodules: true
fetchDepth: 1 fetchDepth: 1
@@ -1087,6 +926,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --lint - bash: ./build.sh --lint
@@ -1102,122 +942,84 @@ stages:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.windowsImage }}
steps: steps:
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@3 - task: SonarCloudPrepare@1
env: env:
SONAR_SCANNER_OPTS: '' SONAR_SCANNER_OPTS: ''
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'readarr' organization: 'readarr'
scannerMode: 'cli' scannerMode: 'CLI'
configMode: 'manual' configMode: 'manual'
cliProjectKey: 'readarrui' cliProjectKey: 'readarrui'
cliProjectName: 'ReadarrUI' cliProjectName: 'ReadarrUI'
cliProjectVersion: '$(readarrVersion)' cliProjectVersion: '$(readarrVersion)'
cliSources: './frontend' 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'))
)
pool:
vmImage: ${{ variables.windowsImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: self
submodules: true
persistCredentials: true
fetchDepth: 1
- bash: ./docs.sh Windows
displayName: Create openapi.json
- bash: |
git config --global user.email "development@lidarr.audio"
git config --global user.name "Servarr"
git checkout -b api-docs
git add .
if git status | grep -q modified
then
git commit -am 'Automated API Docs update'
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
echo "No changes since last run"
fi
displayName: Commit API Doc Change
continueOnError: true
env:
GITHUBTOKEN: $(githubToken)
- task: CopyFiles@2
displayName: 'Copy openapi.json to: $(Build.ArtifactStagingDirectory)'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*openapi.json
TargetFolder: '$(Build.ArtifactStagingDirectory)/api_docs'
- publish: $(Build.ArtifactStagingDirectory)/api_docs
artifact: 'APIDocs'
displayName: Publish API Docs Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
- job: Analyze_Backend - job: Analyze_Backend
displayName: Backend displayName: Backend
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
disable.coverage.autogenerate: 'true' disable.coverage.autogenerate: 'true'
EnableAnalyzers: 'false'
pool: pool:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.linuxImage }}
steps: steps:
- task: UseDotNet@2 - 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: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
submodules: true submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual - task: Cache@2
displayName: Enable Windows Test Service inputs:
- task: SonarCloudPrepare@3 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') condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'readarr' organization: 'readarr'
scannerMode: 'dotnet' scannerMode: 'MSBuild'
projectKey: 'Readarr_Readarr' projectKey: 'Readarr_Readarr'
projectName: 'Readarr' projectName: 'Readarr'
projectVersion: '$(readarrVersion)' projectVersion: '$(readarrVersion)'
extraProperties: | 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.coverage.exclusions=**/Readarr.Api.V1/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: | - bash: |
./build.sh --backend -f net6.0 -r win-x64 ./build.sh --backend -f net6.0 -r linux-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@3 env:
NUGET_PACKAGES: $(nugetCacheFolder)
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results displayName: Publish SonarCloud Results
- task: reportgenerator@5.3.11 - task: reportgenerator@4
displayName: Generate Coverage Report displayName: Generate Coverage Report
inputs: inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined' targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges' 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 - stage: Report_Out
dependsOn: dependsOn:
@@ -1225,6 +1027,7 @@ stages:
- Unit_Test - Unit_Test
- Integration - Integration
- Automation - Automation
- Build_Backend_Other
condition: eq(variables['system.pullrequest.isfork'], false) condition: eq(variables['system.pullrequest.isfork'], false)
displayName: Build Status Report displayName: Build Status Report
jobs: jobs:
@@ -1248,4 +1051,3 @@ stages:
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId) DISCORDTHREADID: $(discordThreadId)

View File

@@ -23,7 +23,7 @@ UpdateVersionNumber()
echo "Updating Version Info" echo "Updating Version Info"
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props 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/<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 fi
} }
@@ -183,7 +183,7 @@ PackageMacOSApp()
rm -rf $folder rm -rf $folder
mkdir -p $folder mkdir -p $folder
cp -r distribution/osx/Readarr.app $folder cp -r macOS/Readarr.app $folder
mkdir -p $folder/Readarr.app/Contents/MacOS mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries" echo "Copying Binaries"
@@ -245,7 +245,7 @@ BuildInstaller()
local framework="$1" local framework="$1"
local runtime="$2" 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() InstallInno()
@@ -391,21 +391,22 @@ then
fi fi
fi fi
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]]; if [ "$FRONTEND" = "YES" ];
then then
YarnInstall YarnInstall
RunWebpack
fi fi
if [ "$LINT" = "YES" ]; if [ "$LINT" = "YES" ];
then then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI LintUI
fi fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ]; if [ "$PACKAGES" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber

48
docs.sh
View File

@@ -1,48 +0,0 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1
if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-x64"
elif [ "$PLATFORM" = "Linux" ]; then
RUNTIME="linux-x64"
elif [ "$PLATFORM" = "Mac" ]; then
RUNTIME="osx-x64"
else
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
exit 1
fi
outputFolder='_output'
testPackageFolder='_tests'
rm -rf $outputFolder
rm -rf $testPackageFolder
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 run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
sleep 45
kill %1
exit 0

25
frontend/.csscomb.json Normal file
View File

@@ -0,0 +1,25 @@
{
"remove-empty-rulesets": true,
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
"color-shorthand": false,
"element-case": "lower",
"eof-newline": true,
"leading-zero": true,
"quotes": "double",
"sort-order-fallback": "abc",
"space-before-colon": "",
"space-after-colon": " ",
"space-before-combinator": " ",
"space-after-combinator": " ",
"space-between-declarations": "\n",
"space-before-opening-brace": " ",
"space-after-opening-brace": "\n",
"space-after-selector-delimiter": " ",
"space-before-selector-delimiter": "",
"space-before-closing-brace": "\n",
"strip-spaces": true,
"tab-size": true,
"unitless-zero": false
}

335
frontend/.esformatter Normal file
View File

@@ -0,0 +1,335 @@
{
"indent": {
"value": " ",
"FunctionExpression": 1,
"ArrayExpression": 1,
"ObjectExpression": 1
},
"lineBreak": {
"value": "\n",
"before": {
"ArrayPatternClosing": 0,
"ArrayPatternComma": 0,
"ArrayPatternOpening": 0,
"ArrowFunctionExpressionArrow": 0,
"ArrowFunctionExpressionClosingBrace": ">=1",
"ArrowFunctionExpressionOpeningBrace": 0,
"AssignmentExpression": ">=1",
"AssignmentOperator": 0,
"BlockStatement": 0,
"BreakKeyword": ">=1",
"CallExpression": -1,
"CallExpressionClosingParentheses": -1,
"CallExpressionOpeningParentheses": 0,
"CatchClosingBrace": ">=1",
"CatchKeyword": 0,
"CatchOpeningBrace": 0,
"ClassDeclaration": ">=1",
"ClassDeclarationClosingBrace": ">=1",
"ClassDeclarationOpeningBrace": 0,
"ConditionalExpression": ">=1",
"DeleteOperator": ">=1",
"DoWhileStatement": ">=1",
"DoWhileStatementClosingBrace": ">=1",
"DoWhileStatementOpeningBrace": 0,
"ElseIfStatement": 0,
"ElseIfStatementClosingBrace": ">=1",
"ElseIfStatementOpeningBrace": 0,
"ElseStatement": 0,
"ElseStatementClosingBrace": ">=1",
"ElseStatementOpeningBrace": 0,
"EmptyStatement": -1,
"EndOfFile": -1,
"FinallyClosingBrace": ">=1",
"FinallyKeyword": -1,
"FinallyOpeningBrace": 0,
"ForInStatement": ">=1",
"ForInStatementClosingBrace": ">=1",
"ForInStatementExpressionClosing": 0,
"ForInStatementExpressionOpening": 0,
"ForInStatementOpeningBrace": 0,
"ForStatement": ">=1",
"ForStatementClosingBrace": ">=1",
"ForStatementExpressionClosing": "<2",
"ForStatementExpressionOpening": 0,
"ForStatementOpeningBrace": 0,
"FunctionDeclaration": ">=1",
"FunctionDeclarationClosingBrace": ">=1",
"FunctionDeclarationOpeningBrace": 0,
"FunctionExpression": 0,
"FunctionExpressionClosingBrace": 1,
"FunctionExpressionOpeningBrace":0,
"IIFEClosingParentheses": 0,
"IfStatement": ">=1",
"IfStatementClosingBrace": ">=1",
"IfStatementOpeningBrace": 0,
"LogicalExpression": -1,
"MemberExpressionClosing": 0,
"MemberExpressionOpening": 0,
"MemberExpressionPeriod": -1,
"MethodDefinition": ">=1",
"ObjectExpressionClosingBrace": "<=1",
"ObjectPatternClosingBrace": 0,
"ObjectPatternComma": 0,
"ObjectPatternOpeningBrace": 0,
"ParameterDefault": 0,
"Property": "<=2",
"PropertyValue": 0,
"ReturnStatement": -1,
"SwitchClosingBrace": ">=1",
"SwitchOpeningBrace": 0,
"ThisExpression": -1,
"ThrowStatement": ">=1",
"TryClosingBrace": ">=1",
"TryKeyword": -1,
"TryOpeningBrace": 0,
"VariableDeclaration": ">=1",
"VariableDeclarationSemiColon": 0,
"VariableDeclarationWithoutInit": ">=1",
"VariableName": ">=1",
"VariableValue": 0,
"WhileStatement": ">=1",
"WhileStatementClosingBrace": ">=1",
"WhileStatementOpeningBrace": 0
},
"after": {
"ArrayPatternClosing": 0,
"ArrayPatternComma": 0,
"ArrayPatternOpening": 0,
"ArrowFunctionExpressionArrow": 0,
"ArrowFunctionExpressionClosingBrace": -1,
"ArrowFunctionExpressionOpeningBrace": ">=1",
"AssignmentExpression": ">=1",
"AssignmentOperator": 0,
"BlockStatement": 0,
"BreakKeyword": -1,
"CallExpression": -1,
"CallExpressionClosingParentheses": -1,
"CallExpressionOpeningParentheses": -1,
"CatchClosingBrace": ">=0",
"CatchKeyword": 0,
"CatchOpeningBrace": ">=1",
"ClassDeclaration": ">=1",
"ClassDeclarationClosingBrace": ">=1",
"ClassDeclarationOpeningBrace": ">=1",
"ConditionalExpression": ">=1",
"DeleteOperator": ">=1",
"DoWhileStatement": ">=1",
"DoWhileStatementClosingBrace": 0,
"DoWhileStatementOpeningBrace": ">=1",
"ElseIfStatement": ">=1",
"ElseIfStatementClosingBrace": ">=1",
"ElseIfStatementOpeningBrace": ">=1",
"ElseStatement": ">=1",
"ElseStatementClosingBrace": ">=1",
"ElseStatementOpeningBrace": ">=1",
"EmptyStatement": -1,
"FinallyClosingBrace": ">=1",
"FinallyKeyword": -1,
"FinallyOpeningBrace": ">=1",
"ForInStatement": ">=1",
"ForInStatementClosingBrace": ">=1",
"ForInStatementExpressionClosing": -1,
"ForInStatementExpressionOpening": "<2",
"ForInStatementOpeningBrace": ">=1",
"ForStatement": ">=1",
"ForStatementClosingBrace": ">=1",
"ForStatementExpressionClosing": -1,
"ForStatementExpressionOpening": "<2",
"ForStatementOpeningBrace": ">=1",
"FunctionDeclaration": ">=1",
"FunctionDeclarationClosingBrace": ">=1",
"FunctionDeclarationOpeningBrace": ">=1",
"FunctionExpression": 0,
"FunctionExpressionClosingBrace": -1,
"FunctionExpressionOpeningBrace": 1,
"IIFEOpeningParentheses": 0,
"IfStatement": ">=1",
"IfStatementClosingBrace": ">=1",
"IfStatementOpeningBrace": ">=1",
"LogicalExpression": -1,
"MemberExpressionClosing": 0,
"MemberExpressionOpening": 0,
"MemberExpressionPeriod": 0,
"MethodDefinition": ">=1",
"ObjectExpressionOpeningBrace": "<=1",
"ObjectPatternClosingBrace": 0,
"ObjectPatternComma": 0,
"ObjectPatternOpeningBrace": 0,
"ParameterDefault": 0,
"Property": -1,
"PropertyName": 0,
"ReturnStatement": -1,
"SwitchCaseColon": ">=1",
"SwitchClosingBrace": ">=1",
"SwitchOpeningBrace": ">=1",
"ThisExpression": 0,
"ThrowStatement": ">=1",
"TryClosingBrace": 0,
"TryKeyword": -1,
"TryOpeningBrace": ">=1",
"VariableDeclaration": ">=1",
"VariableDeclarationSemiColon": ">=1",
"VariableValue": -1,
"WhileStatement": ">=1",
"WhileStatementClosingBrace": ">=1",
"WhileStatementOpeningBrace": ">=1"
}
},
"whiteSpace": {
"value": " ",
"removeTrailing": 1,
"before": {
"ArgumentComma": 0,
"ArgumentList": 0,
"ArgumentListArrayExpression": 0,
"ArgumentListFunctionExpression": 1,
"ArgumentListObjectExpression": 0,
"ArrayExpressionClosing": 0,
"ArrayExpressionComma": 0,
"ArrayExpressionOpening": 1,
"AssignmentOperator": 1,
"BinaryExpression": 0,
"BinaryExpressionOperator": 1,
"BlockComment": 1,
"CallExpression": 1,
"CatchClosingBrace": 1,
"CatchKeyword": 1,
"CatchOpeningBrace": 1,
"CatchParameterList": 0,
"CommaOperator": 0,
"ConditionalExpressionAlternate": 1,
"ConditionalExpressionConsequent": 1,
"DoWhileStatementClosingBrace": 1,
"DoWhileStatementConditional": 1,
"DoWhileStatementOpeningBrace": 1,
"ElseIfStatementClosingBrace": 1,
"ElseIfStatementOpeningBrace": 1,
"ElseStatementClosingBrace": 1,
"ElseStatementOpeningBrace": 1,
"EmptyStatement": 0,
"ExpressionClosingParentheses": 0,
"FinallyClosingBrace": 1,
"FinallyKeyword": -1,
"FinallyOpeningBrace": 1,
"ForInStatement": 1,
"ForInStatementClosingBrace": 1,
"ForInStatementExpressionClosing": 0,
"ForInStatementExpressionOpening": 1,
"ForInStatementOpeningBrace": 1,
"ForStatement": 1,
"ForStatementClosingBrace": 1,
"ForStatementExpressionClosing": 0,
"ForStatementExpressionOpening": 1,
"ForStatementOpeningBrace": 1,
"ForStatementSemicolon": 0,
"FunctionDeclarationClosingBrace": 1,
"FunctionDeclarationOpeningBrace": 1,
"FunctionExpressionClosingBrace": 1,
"FunctionExpressionOpeningBrace": 1,
"IfStatementClosingBrace": 1,
"IfStatementConditionalClosing": 0,
"IfStatementConditionalOpening": 1,
"IfStatementOpeningBrace": 1,
"LineComment": 1,
"LogicalExpressionOperator": 1,
"MemberExpressionClosing": 0,
"ObjectExpressionClosingBrace": 1,
"ParameterComma": 0,
"ParameterList": 0,
"Property": 1,
"PropertyName": 1,
"PropertyValue": 1,
"SwitchDiscriminantClosing": 0,
"SwitchDiscriminantOpening": 1,
"ThrowKeyword": 1,
"TryClosingBrace": 1,
"TryKeyword": -1,
"TryOpeningBrace": 1,
"UnaryExpressionOperator": 0,
"VariableName": 1,
"VariableValue": 1,
"WhileStatementClosingBrace": 1,
"WhileStatementConditionalClosing": 0,
"WhileStatementConditionalOpening": 1,
"WhileStatementOpeningBrace": 1
},
"after": {
"ArgumentComma": 1,
"ArgumentList": 0,
"ArgumentListArrayExpression": 1,
"ArgumentListFunctionExpression": 1,
"ArgumentListObjectExpression": 0,
"ArrayExpressionClosing": 0,
"ArrayExpressionComma": 1,
"ArrayExpressionOpening": 0,
"AssignmentOperator": 1,
"BinaryExpression": 0,
"BinaryExpressionOperator": 1,
"BlockComment": 1,
"CallExpression": 0,
"CatchClosingBrace": 1,
"CatchKeyword": 1,
"CatchOpeningBrace": 1,
"CatchParameterList": 0,
"CommaOperator": 1,
"ConditionalExpressionConsequent": 1,
"ConditionalExpressionTest": 1,
"DoWhileStatementBody": 1,
"DoWhileStatementClosingBrace": 1,
"DoWhileStatementOpeningBrace": 1,
"ElseIfStatementClosingBrace": 1,
"ElseIfStatementOpeningBrace": 1,
"ElseStatementClosingBrace": 1,
"ElseStatementOpeningBrace": 1,
"EmptyStatement": 0,
"ExpressionOpeningParentheses": 0,
"FinallyClosingBrace": 1,
"FinallyKeyword": -1,
"FinallyOpeningBrace": 1,
"ForInStatement": 1,
"ForInStatementClosingBrace": 1,
"ForInStatementExpressionClosing": 1,
"ForInStatementExpressionOpening": 0,
"ForInStatementOpeningBrace": 1,
"ForStatement": 1,
"ForStatementClosingBrace": 1,
"ForStatementExpressionClosing": 1,
"ForStatementExpressionOpening": 0,
"ForStatementOpeningBrace": 1,
"ForStatementSemicolon": 1,
"FunctionDeclarationClosingBrace": 0,
"FunctionDeclarationOpeningBrace": 0,
"FunctionExpressionClosingBrace": 0,
"FunctionExpressionOpeningBrace": 0,
"FunctionName": 0,
"FunctionReservedWord": 0,
"IfStatementClosingBrace": 1,
"IfStatementConditionalClosing": 0,
"IfStatementConditionalOpening": 0,
"IfStatementOpeningBrace": 1,
"LogicalExpressionOperator": 1,
"MemberExpressionOpening": 0,
"ObjectExpressionClosingBrace": 0,
"ObjectExpressionOpeningBrace": 1,
"ParameterComma": 1,
"ParameterList": 0,
"PropertyName": 0,
"PropertyValue": 0,
"SwitchDiscriminantClosing": 1,
"SwitchDiscriminantOpening": 0,
"ThrowKeyword": 1,
"TryClosingBrace": 1,
"TryKeyword": -1,
"TryOpeningBrace": 1,
"UnaryExpressionOperator": 0,
"VariableName": 1,
"WhileStatementClosingBrace": 1,
"WhileStatementConditionalClosing": 1,
"WhileStatementConditionalOpening": 0,
"WhileStatementOpeningBrace": 1
}
}
}

View File

@@ -1,2 +1 @@
**/JsLibraries/** **/JsLibraries/**
**/*.css.d.ts

View File

@@ -1,21 +1,14 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs'); const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin').configs.recommended;
const frontendFolder = __dirname;
const dirs = fs const dirs = fs
.readdirSync(path.join(frontendFolder, 'src'), { withFileTypes: true }) .readdirSync('frontend/src', { withFileTypes: true })
.filter((dirent) => dirent.isDirectory()) .filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name) .map((dirent) => dirent.name)
.join('|'); .join('|');
module.exports = { const frontendFolder = __dirname;
root: true,
module.exports = {
parser: '@babel/eslint-parser', parser: '@babel/eslint-parser',
env: { env: {
@@ -35,7 +28,7 @@ module.exports = {
ecmaVersion: 6, ecmaVersion: 6,
sourceType: 'module', sourceType: 'module',
babelOptions: { babelOptions: {
configFile: `${frontendFolder}/babel.config.js` configFile: `${frontendFolder}/babel.config.js`,
}, },
ecmaFeatures: { ecmaFeatures: {
modules: true, modules: true,
@@ -48,9 +41,7 @@ module.exports = {
'react', 'react',
'react-hooks', 'react-hooks',
'simple-import-sort', 'simple-import-sort',
'import', 'import'
'@typescript-eslint',
'prettier'
], ],
settings: { settings: {
@@ -233,7 +224,7 @@ module.exports = {
'consistent-this': ['error', 'self'], 'consistent-this': ['error', 'self'],
'eol-last': 'error', 'eol-last': 'error',
'func-names': 'off', 'func-names': 'off',
'func-style': ['error', 'declaration', { allowArrowFunctions: true }], 'func-style': ['error', 'declaration'],
indent: ['error', 2, { SwitchCase: 1 }], indent: ['error', 2, { SwitchCase: 1 }],
'key-spacing': ['error', { beforeColon: false, afterColon: true }], 'key-spacing': ['error', { beforeColon: false, afterColon: true }],
'keyword-spacing': ['error', { before: true, after: true }], 'keyword-spacing': ['error', { before: true, after: true }],
@@ -324,9 +315,7 @@ module.exports = {
}, },
overrides: [ overrides: [
{ {
files: [ files: ['*.js'],
'*.js'
],
rules: { rules: {
'simple-import-sort/imports': [ 'simple-import-sort/imports': [
'error', 'error',
@@ -341,52 +330,6 @@ module.exports = {
} }
] ]
} }
},
{
files: [
'*.ts',
'*.tsx'
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json'
},
extends: [
'prettier'
],
rules: Object.assign(typescriptEslintRecommended.rules, {
'no-shadow': 'off',
// These should be enabled after cleaning things up
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'prettier/prettier': 'error',
'simple-import-sort/imports': [
'error',
{
groups: [
// Packages
// Absolute Paths
// Relative Paths
// Css
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
]
}
]
})
},
{
files: [
'*.css.d.ts'
],
rules: {
'filenames/match-exported': 'off',
'init-declarations': 'off',
'prettier/prettier': 'off'
}
} }
] ]
}; };

12
frontend/.jsbeautifyrc Normal file
View File

@@ -0,0 +1,12 @@
{
"js": {
"indent_size": 2,
"indent_char": " ",
"indent_level": 2,
"indent_with_tabs": false,
"preserve_newlines": true,
"brace_style": "collapse",
"max_preserve_newlines": 2,
"jslint_happy": true
}
}

View File

@@ -1,10 +0,0 @@
# Ignore everything recursively
*
# But not the .ts files
!*.ts*
*css.d.ts
# Check subdirectories too
!*/

View File

@@ -1,6 +0,0 @@
{
"arrowParens": "always",
"endOfLine": "auto",
"singleQuote": true,
"trailingComma": "es5"
}

View File

@@ -1,12 +1,12 @@
{ {
"plugins": [ "plugins": [
"stylelint-order" "stylelint-order"
], ],
"ignoreFiles": [ "ignoreFiles": [
"frontend/src/Styles/scaffolding.css", "frontend/src/Styles/scaffolding.css",
"**/*.js" "**/*.js"
], ],
"rules": { "rules": {
"at-rule-empty-line-before": [ "at-rule-empty-line-before": [
"always", "always",
{ {
@@ -15,6 +15,9 @@
] ]
} }
], ],
"at-rule-name-case": "lower",
"at-rule-name-newline-after": "always-multi-line",
"at-rule-name-space-after": "always",
"at-rule-no-unknown": [ "at-rule-no-unknown": [
true, true,
{ {
@@ -25,36 +28,83 @@
} }
], ],
"at-rule-no-vendor-prefix": true, "at-rule-no-vendor-prefix": true,
"at-rule-semicolon-newline-after": "always",
"at-rule-semicolon-space-before": "never",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always",
"block-closing-brace-space-after": "always-single-line",
"block-closing-brace-space-before": "always-single-line",
"block-no-empty": true, "block-no-empty": true,
"block-opening-brace-newline-after": "always",
"block-opening-brace-newline-before": "never-single-line",
"block-opening-brace-space-after": "always-single-line",
"block-opening-brace-space-before": "always",
"color-hex-case": "lower",
"color-hex-length": "short", "color-hex-length": "short",
"color-named": "never", "color-named": "never",
"color-no-invalid-hex": true, "color-no-invalid-hex": true,
"comment-whitespace-inside": "always", "comment-whitespace-inside": "always",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": [ "declaration-block-no-duplicate-properties": [
true, true,
{ {
"ignoreProperties": [ "ignoreProperties": [
"composes" "composes"
] ]
} }
], ],
"declaration-block-no-redundant-longhand-properties": true, "declaration-block-no-redundant-longhand-properties": true,
"declaration-block-no-shorthand-property-overrides": true, "declaration-block-no-shorthand-property-overrides": true,
"declaration-block-semicolon-newline-after": "always",
"declaration-block-semicolon-newline-before": "never-multi-line",
"declaration-block-semicolon-space-before": "never",
"declaration-block-single-line-max-declarations": 1, "declaration-block-single-line-max-declarations": 1,
"declaration-block-trailing-semicolon": "always",
"declaration-colon-space-after": "always",
"declaration-colon-space-before": "never",
"font-family-name-quotes": "always-unless-keyword", "font-family-name-quotes": "always-unless-keyword",
"function-calc-no-unspaced-operator": true, "function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "never-multi-line",
"function-comma-newline-before": "never-multi-line",
"function-comma-space-after": "always",
"function-comma-space-before": "never",
"function-linear-gradient-no-nonstandard-direction": true, "function-linear-gradient-no-nonstandard-direction": true,
"function-name-case": "lower", "function-name-case": "lower",
"function-parentheses-newline-inside": "never-multi-line",
"function-parentheses-space-inside": "never",
"function-url-quotes": "always", "function-url-quotes": "always",
"function-url-scheme-disallowed-list": [ "function-url-scheme-disallowed-list": [
"data" "data"
], ],
"function-whitespace-after": "always",
"indentation": 2,
"keyframe-declaration-no-important": true, "keyframe-declaration-no-important": true,
"length-zero-no-unit": true, "length-zero-no-unit": true,
"max-empty-lines": 1,
"max-line-length": [
100,
{
"ignore": [
"non-comments"
]
}
],
"max-nesting-depth": 2, "max-nesting-depth": 2,
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-vendor-prefix": true, "media-feature-name-no-vendor-prefix": true,
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"no-empty-source": true, "no-empty-source": true,
"no-eol-whitespace": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true, "no-invalid-double-slash-comments": true,
"no-missing-end-of-source-newline": true,
"number-leading-zero": "always",
"number-no-trailing-zeros": true,
"order/order": [ "order/order": [
"custom-properties", "custom-properties",
"dollar-variables", "dollar-variables",
@@ -82,7 +132,6 @@
"right", "right",
"bottom", "bottom",
"left", "left",
"inset",
"z-index", "z-index",
"display", "display",
"visibility", "visibility",
@@ -294,33 +343,54 @@
] ]
} }
], ],
"property-case": "lower",
"property-no-vendor-prefix": true, "property-no-vendor-prefix": true,
"rule-empty-line-before": [ "rule-empty-line-before": [
"always", "always",
{ {
"except": [ "except": [
"first-nested" "first-nested"
], ],
"ignore": [ "ignore": [
"after-comment" "after-comment"
] ]
} }
], ],
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-attribute-quotes": "never", "selector-attribute-quotes": "never",
"selector-class-pattern": "^[A-Za-z0-9]+$", "selector-class-pattern": "^[A-Za-z0-9]+$",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-descendant-combinator-no-non-space": true,
"selector-list-comma-newline-after": "always",
"selector-list-comma-newline-before": "never-multi-line",
"selector-list-comma-space-before": "never",
"selector-max-attribute": 0, "selector-max-attribute": 0,
"selector-max-class": 3, "selector-max-class": 3,
"selector-max-compound-selectors": 3, "selector-max-compound-selectors": 3,
"selector-max-empty-lines": 0,
"selector-max-id": 0, "selector-max-id": 0,
"selector-max-universal": 0, "selector-max-universal": 0,
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-colon-notation": "double", "selector-pseudo-element-colon-notation": "double",
"selector-pseudo-element-no-unknown": true, "selector-pseudo-element-no-unknown": true,
"selector-type-case": "lower", "selector-type-case": "lower",
"selector-type-no-unknown": true, "selector-type-no-unknown": true,
"shorthand-property-no-redundant-values": true, "shorthand-property-no-redundant-values": true,
"string-no-newline": true, "string-no-newline": true,
"string-quotes": "single",
"time-min-milliseconds": 100, "time-min-milliseconds": 100,
"unit-case": "lower",
"unit-no-unknown": true, "unit-no-unknown": true,
"value-list-comma-newline-after": "never-multi-line",
"value-list-comma-newline-before": "never-multi-line",
"value-list-comma-space-after": "always",
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0,
"value-no-vendor-prefix": true "value-no-vendor-prefix": true
} }
} }

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"stylelint.vscode-stylelint",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@@ -1,23 +0,0 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.insertFinalNewline": true,
"files.exclude": {
"**/node_modules": true,
"**/*.d.css": true
},
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"typescript.preferences.quoteStyle": "single",
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
}

View File

@@ -2,25 +2,22 @@ const loose = true;
module.exports = { module.exports = {
plugins: [ plugins: [
'@babel/plugin-transform-logical-assignment-operators',
// Stage 1 // Stage 1
'@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-export-default-from',
['@babel/plugin-transform-optional-chaining', { loose }], ['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose }], ['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
// Stage 2 // Stage 2
'@babel/plugin-transform-export-namespace-from', '@babel/plugin-proposal-export-namespace-from',
// Stage 3 // Stage 3
['@babel/plugin-transform-class-properties', { loose }], ['@babel/plugin-proposal-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import' '@babel/plugin-syntax-dynamic-import'
], ],
env: { env: {
development: { development: {
presets: [ presets: [
['@babel/preset-react', { development: true }], ['@babel/preset-react', { development: true }]
'@babel/preset-typescript'
], ],
plugins: [ plugins: [
'babel-plugin-inline-classnames' 'babel-plugin-inline-classnames'
@@ -28,8 +25,7 @@ module.exports = {
}, },
production: { production: {
presets: [ presets: [
'@babel/preset-react', '@babel/preset-react'
'@babel/preset-typescript'
], ],
plugins: [ plugins: [
'babel-plugin-transform-react-remove-prop-types' 'babel-plugin-transform-react-remove-prop-types'

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const FileManagerPlugin = require('filemanager-webpack-plugin'); const FileManagerPlugin = require('filemanager-webpack-plugin');
@@ -6,7 +5,6 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin'); const LiveReloadPlugin = require('webpack-livereload-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = (env) => { module.exports = (env) => {
const uiFolder = 'UI'; const uiFolder = 'UI';
@@ -26,7 +24,6 @@ module.exports = (env) => {
const config = { const config = {
mode: isProduction ? 'production' : 'development', mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map', devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: { stats: {
children: false children: false
@@ -37,22 +34,17 @@ module.exports = (env) => {
}, },
entry: { entry: {
index: 'index.ts' index: 'index.js'
}, },
resolve: { resolve: {
extensions: [
'.ts',
'.tsx',
'.js'
],
modules: [ modules: [
srcFolder, srcFolder,
path.join(srcFolder, 'Shims'), path.join(srcFolder, 'Shims'),
'node_modules' 'node_modules'
], ],
alias: { alias: {
jquery: 'jquery/dist/jquery.min', jquery: 'jquery/src/jquery',
'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate' 'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate'
}, },
fallback: { fallback: {
@@ -68,23 +60,23 @@ module.exports = (env) => {
output: { output: {
path: distFolder, path: distFolder,
publicPath: '/', publicPath: '/',
filename: isProduction ? '[name]-[contenthash].js' : '[name].js', filename: '[name].js',
sourceMapFilename: '[file].map' sourceMapFilename: '[file].map'
}, },
optimization: { optimization: {
moduleIds: 'deterministic', moduleIds: 'deterministic',
chunkIds: isProduction ? 'deterministic' : 'named' chunkIds: 'named',
splitChunks: {
chunks: 'initial',
name: 'vendors'
}
}, },
performance: { performance: {
hints: false hints: false
}, },
experiments: {
topLevelAwait: true
},
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__DEV__: !isProduction, __DEV__: !isProduction,
@@ -92,15 +84,13 @@ module.exports = (env) => {
}), }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: 'Content/styles.css', filename: 'Content/styles.css'
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs', template: 'frontend/src/index.ejs',
filename: 'index.html', filename: 'index.html',
publicPath: '/', publicPath: '/'
inject: false
}), }),
new FileManagerPlugin({ new FileManagerPlugin({
@@ -141,8 +131,6 @@ module.exports = (env) => {
} }
}), }),
new ForkTsCheckerWebpackPlugin(),
new LiveReloadPlugin() new LiveReloadPlugin()
], ],
@@ -166,7 +154,7 @@ module.exports = (env) => {
} }
}, },
{ {
test: [/\.jsx?$/, /\.tsx?$/], test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/, exclude: /(node_modules|JsLibraries)/,
use: [ use: [
{ {
@@ -182,7 +170,7 @@ module.exports = (env) => {
loose: true, loose: true,
debug: false, debug: false,
useBuiltIns: 'entry', useBuiltIns: 'entry',
corejs: '3.39' corejs: 3
} }
] ]
] ]
@@ -197,13 +185,12 @@ module.exports = (env) => {
exclude: /(node_modules|globals.css)/, exclude: /(node_modules|globals.css)/,
use: [ use: [
{ loader: MiniCssExtractPlugin.loader }, { loader: MiniCssExtractPlugin.loader },
{ loader: 'css-modules-typescript-loader' },
{ {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
importLoaders: 1, importLoaders: 1,
modules: { modules: {
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]' localIdentName: '[name]/[local]/[hash:base64:5]'
} }
} }
}, },
@@ -266,19 +253,18 @@ module.exports = (env) => {
config.resolve.alias['react-dom$'] = 'react-dom/profiling'; config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling'; config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization = { config.optimization.minimizer = [
minimize: true, new TerserPlugin({
minimizer: [ cache: true,
new TerserPlugin({ parallel: true,
terserOptions: { sourceMap: true, // Must be set to true if using source-maps in production
sourceMap: true, // Must be set to true if using source-maps in production terserOptions: {
mangle: false, mangle: false,
keep_classnames: true, keep_classnames: true,
keep_fnames: true keep_fnames: true
} }
}) })
] ];
};
} }
return config; return config;

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line filenames/match-exported
const loaderUtils = require('loader-utils'); const loaderUtils = require('loader-utils');
module.exports = function cssVariablesLoader(source) { module.exports = function cssVariablesLoader(source) {

4
frontend/src/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.insertFinalNewline": true
}

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@@ -162,16 +161,16 @@ class Blocklist extends Component {
{ {
!isAnyFetching && !!error && !isAnyFetching && !!error &&
<Alert kind={kinds.DANGER}> <div>
{translate('UnableToLoadBlocklist')} {translate('UnableToLoadBlocklist')}
</Alert> </div>
} }
{ {
isAllPopulated && !error && !items.length && isAllPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}> <div>
{translate('NoHistoryBlocklist')} {translate('NoHistoryBlocklist')}
</Alert> </div>
} }
{ {
@@ -215,7 +214,7 @@ class Blocklist extends Component {
isOpen={isConfirmRemoveModalOpen} isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('RemoveSelected')} title={translate('RemoveSelected')}
message={translate('RemoveSelectedItemBlocklistMessageText')} message={translate('RemoveSelectedMessageText')}
confirmLabel={translate('RemoveSelected')} confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed} onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose} onCancel={this.onConfirmRemoveModalClose}

View File

@@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
'indexer': string;
'quality': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'description': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge'; import formatAge from 'Utilities/Number/formatAge';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css'; import styles from './HistoryDetails.css';
@@ -107,8 +107,8 @@ function HistoryDetails(props) {
{ {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title="Custom Format Score"
data={formatCustomFormatScore(customFormatScore)} data={formatPreferredWordScore(customFormatScore)}
/> : /> :
null null
} }
@@ -165,8 +165,7 @@ function HistoryDetails(props) {
if (eventType === 'downloadFailed') { if (eventType === 'downloadFailed') {
const { const {
message, message
indexer
} = data; } = data;
return ( return (
@@ -178,21 +177,11 @@ function HistoryDetails(props) {
/> />
{ {
indexer ? !!message &&
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/> :
null
}
{
message ?
<DescriptionListItem <DescriptionListItem
title={translate('Message')} title={translate('Message')}
data={message} data={message}
/> : />
null
} }
</DescriptionList> </DescriptionList>
); );
@@ -235,8 +224,8 @@ function HistoryDetails(props) {
{ {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title="Custom Format Score"
data={formatCustomFormatScore(customFormatScore)} data={formatPreferredWordScore(customFormatScore)}
/> : /> :
null null
} }
@@ -281,8 +270,8 @@ function HistoryDetails(props) {
{ {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title="Custom Format Score"
data={formatCustomFormatScore(customFormatScore)} data={formatPreferredWordScore(customFormatScore)}
/> : /> :
null null
} }

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'markAsFailedButton': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu'; import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@@ -12,7 +11,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props'; import { align, icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import HistoryRowConnector from './HistoryRowConnector'; import HistoryRowConnector from './HistoryRowConnector';
@@ -86,9 +85,9 @@ class History extends Component {
{ {
!isFetchingAny && hasError && !isFetchingAny && hasError &&
<Alert kind={kinds.DANGER}> <div>
{translate('UnableToLoadHistory')} {translate('UnableToLoadHistory')}
</Alert> </div>
} }
{ {
@@ -96,9 +95,9 @@ class History extends Component {
// wait for the books to populate because they are never coming. // wait for the books to populate because they are never coming.
isPopulated && !hasError && !items.length && isPopulated && !hasError && !items.length &&
<Alert kind={kinds.INFO}> <div>
{translate('NoHistory')} No history found
</Alert> </div>
} }
{ {

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cell': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,11 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'customFormatScore': string;
'details': string;
'downloadClient': string;
'indexer': string;
'releaseGroup': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -8,9 +8,8 @@ import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip'; import { icons } from 'Helpers/Props';
import { icons, tooltipPositions } from 'Helpers/Props'; import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css'; import styles from './HistoryRow.css';
@@ -58,7 +57,6 @@ class HistoryRow extends Component {
book, book,
quality, quality,
customFormats, customFormats,
customFormatScore,
qualityCutoffNotMet, qualityCutoffNotMet,
eventType, eventType,
sourceTitle, sourceTitle,
@@ -179,14 +177,7 @@ class HistoryRow extends Component {
key={name} key={name}
className={styles.customFormatScore} className={styles.customFormatScore}
> >
<Tooltip {formatPreferredWordScore(data.customFormatScore)}
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell> </TableRowCell>
); );
} }
@@ -218,12 +209,10 @@ class HistoryRow extends Component {
key={name} key={name}
className={styles.details} className={styles.details}
> >
<div className={styles.actionContents}> <IconButton
<IconButton name={icons.INFO}
name={icons.INFO} onPress={this.onDetailsPress}
onPress={this.onDetailsPress} />
/>
</div>
</TableRowCell> </TableRowCell>
); );
} }
@@ -255,7 +244,6 @@ HistoryRow.propTypes = {
book: PropTypes.object, book: PropTypes.object,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
@@ -269,8 +257,4 @@ HistoryRow.propTypes = {
onMarkAsFailedPress: PropTypes.func.isRequired onMarkAsFailedPress: PropTypes.func.isRequired
}; };
HistoryRow.defaultProps = {
customFormats: []
};
export default HistoryRow; export default HistoryRow;

View File

@@ -1,8 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'torrent': string;
'usenet': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,7 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
@@ -13,7 +12,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props'; import { align, icons } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems'; import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
@@ -23,7 +22,7 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueOptionsConnector from './QueueOptionsConnector'; import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector'; import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemModal from './RemoveQueueItemModal'; import RemoveQueueItemsModal from './RemoveQueueItemsModal';
class Queue extends Component { class Queue extends Component {
@@ -234,17 +233,17 @@ class Queue extends Component {
{ {
!isRefreshing && hasError ? !isRefreshing && hasError ?
<Alert kind={kinds.DANGER}> <div>
{translate('FailedToLoadQueue')} {translate('FailedToLoadQueue')}
</Alert> : </div> :
null null
} }
{ {
isAllPopulated && !hasError && !items.length ? isAllPopulated && !hasError && !items.length ?
<Alert kind={kinds.INFO}> <div>
{translate('QueueIsEmpty')} {translate('QueueIsEmpty')}
</Alert> : </div> :
null null
} }
@@ -289,16 +288,9 @@ class Queue extends Component {
} }
</PageContentBody> </PageContentBody>
<RemoveQueueItemModal <RemoveQueueItemsModal
isOpen={isConfirmRemoveModalOpen} isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount} selectedCount={selectedCount}
canChangeCategory={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.downloadClientHasPostImportCategory);
})
)}
canIgnore={isConfirmRemoveModalOpen && ( canIgnore={isConfirmRemoveModalOpen && (
selectedIds.every((id) => { selectedIds.every((id) => {
const item = items.find((i) => i.id === id); const item = items.find((i) => i.id === id);
@@ -306,7 +298,7 @@ class Queue extends Component {
return !!(item && item.authorId && item.bookId); return !!(item && item.authorId && item.bookId);
}) })
)} )}
pending={isConfirmRemoveModalOpen && ( allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => { selectedIds.every((id) => {
const item = items.find((i) => i.id === id); const item = items.find((i) => i.id === id);
@@ -345,8 +337,4 @@ Queue.propTypes = {
onRemoveSelectedPress: PropTypes.func.isRequired onRemoveSelectedPress: PropTypes.func.isRequired
}; };
Queue.defaultProps = {
count: 0
};
export default Queue; export default Queue;

View File

@@ -16,12 +16,6 @@
width: 150px; width: 150px;
} }
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -1,11 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
'customFormatScore': string;
'progress': string;
'protocol': string;
'quality': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -14,11 +14,9 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell'; import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal'; import RemoveQueueItemModal from './RemoveQueueItemModal';
@@ -46,14 +44,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true }); this.setState({ isRemoveQueueItemModalOpen: true });
}; };
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => { onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
const { const {
onRemoveQueueItemPress, onRemoveQueueItemPress,
onQueueRowModalOpenOrClose onQueueRowModalOpenOrClose
} = this.props; } = this.props;
onQueueRowModalOpenOrClose(false); onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipRedownload); onRemoveQueueItemPress(blocklist, skipredownload);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
}; };
@@ -93,12 +91,10 @@ class QueueRow extends Component {
book, book,
quality, quality,
customFormats, customFormats,
customFormatScore,
protocol, protocol,
indexer, indexer,
outputPath, outputPath,
downloadClient, downloadClient,
downloadClientHasPostImportCategory,
downloadForced, downloadForced,
estimatedCompletionTime, estimatedCompletionTime,
timeleft, timeleft,
@@ -226,24 +222,6 @@ class QueueRow extends Component {
); );
} }
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'protocol') { if (name === 'protocol') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
@@ -390,7 +368,6 @@ class QueueRow extends Component {
<RemoveQueueItemModal <RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen} isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title} sourceTitle={title}
canChangeCategory={!!downloadClientHasPostImportCategory}
canIgnore={!!author} canIgnore={!!author}
isPending={isPending} isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed} onRemovePress={this.onRemoveQueueItemModalConfirmed}
@@ -415,12 +392,10 @@ QueueRow.propTypes = {
book: PropTypes.object, book: PropTypes.object,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
outputPath: PropTypes.string, outputPath: PropTypes.string,
downloadClient: PropTypes.string, downloadClient: PropTypes.string,
downloadClientHasPostImportCategory: PropTypes.bool,
downloadForced: PropTypes.bool.isRequired, downloadForced: PropTypes.bool.isRequired,
estimatedCompletionTime: PropTypes.string, estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string, timeleft: PropTypes.string,
@@ -441,7 +416,6 @@ QueueRow.propTypes = {
}; };
QueueRow.defaultProps = { QueueRow.defaultProps = {
customFormats: [],
isGrabbing: false, isGrabbing: false,
isRemoving: false isRemoving: false
}; };

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'status': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
}
export const cssExports: CssExports;
export default cssExports;

View 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('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
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;

View File

@@ -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;

View 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>
Remove Selected Item{selectedCount > 1 ? 's' : ''}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</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>
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
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>
);
}
}
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;

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'timeleft': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -32,7 +32,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs'; import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status'; import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks'; import Tasks from 'System/Tasks/Tasks';
import Updates from 'System/Updates/Updates'; import UpdatesConnector from 'System/Updates/UpdatesConnector';
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector'; import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
@@ -247,7 +247,7 @@ function AppRoutes(props) {
<Route <Route
path="/system/updates" path="/system/updates"
component={Updates} component={UpdatesConnector}
/> />
<Route <Route

View File

@@ -1,7 +1,6 @@
.version { .version {
margin: 0 3px; margin: 0 3px;
font-weight: bold; font-weight: bold;
font-family: var(--defaultFontFamily);
} }
.maintenance { .maintenance {

View File

@@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'changes': string;
'maintenance': string;
'version': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
@@ -65,12 +64,12 @@ function AppUpdatedModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('AppUpdated', { appName: 'Readarr' })} Readarr Updated
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <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> </div>
{ {
@@ -78,14 +77,16 @@ function AppUpdatedModalContent(props) {
<div> <div>
{ {
!update.changes && !update.changes &&
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div> <div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
} }
{ {
!!update.changes && !!update.changes &&
<div> <div>
<div className={styles.changes}> <div className={styles.changes}>
{translate('WhatsNew')} What's new?
</div> </div>
<UpdateChanges <UpdateChanges
@@ -112,14 +113,14 @@ function AppUpdatedModalContent(props) {
<Button <Button
onPress={onSeeChangesPress} onPress={onSeeChangesPress}
> >
{translate('RecentChanges')} Recent Changes
</Button> </Button>
<Button <Button
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Reload')} Reload
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'automatic': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -7,7 +7,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ConnectionLostModal.css'; import styles from './ConnectionLostModal.css';
function ConnectionLostModal(props) { function ConnectionLostModal(props) {
@@ -23,16 +22,16 @@ function ConnectionLostModal(props) {
> >
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('ConnectionLost')} Connection Lost
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
{translate('ConnectionLostToBackend', { appName: 'Readarr' })} Readarr has lost its connection to the backend and will need to be reloaded to restore functionality.
</div> </div>
<div className={styles.automatic}> <div className={styles.automatic}>
{translate('ConnectionLostReconnect', { appName: 'Readarr' })} Readarr will try to connect automatically, or you can click reload below.
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
@@ -40,7 +39,7 @@ function ConnectionLostModal(props) {
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Reload')} Reload
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>

View File

@@ -1,5 +0,0 @@
interface ModelBase {
id: number;
}
export default ModelBase;

View File

@@ -1,48 +0,0 @@
import SortDirection from 'Helpers/Props/SortDirection';
export interface Error {
responseJSON: {
message: string;
};
}
export interface AppSectionDeleteState {
isDeleting: boolean;
deleteError: Error;
}
export interface AppSectionSaveState {
isSaving: boolean;
saveError: Error;
}
export interface PagedAppSectionState {
pageSize: number;
}
export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: {
items: T[];
};
}
export interface AppSectionItemState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
item: T;
}
interface AppSectionState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
sortKey: string;
sortDirection: SortDirection;
}
export default AppSectionState;

View File

@@ -1,60 +0,0 @@
import AuthorsAppState from './AuthorsAppState';
import CommandAppState from './CommandAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption {
id: string;
name: string;
}
export interface FilterBuilderProp<T> {
name: string;
label: string;
type: string;
valueType?: string;
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
}
export interface PropertyFilter {
key: string;
value: boolean | string | number | string[] | number[];
type: string;
}
export interface Filter {
key: string;
label: string;
filers: PropertyFilter[];
}
export interface CustomFilter {
id: number;
type: string;
label: string;
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;
}
export default AppState;

View File

@@ -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;

View File

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

View File

@@ -1,48 +0,0 @@
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';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
export type GeneralAppState = AppSectionItemState<General>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {}
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;
}
export default SettingsAppState;

View File

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

View File

@@ -1,12 +0,0 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
export default TagsAppState;

View File

@@ -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;

View File

@@ -7,10 +7,13 @@ function findImage(images, coverType) {
} }
function getUrl(image, coverType, size) { function getUrl(image, coverType, size) {
const imageUrl = image?.url; if (image) {
// Remove protocol
let url = image.url;
if (imageUrl) { url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
return url;
} }
} }

View File

@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
function AuthorNameLink({ titleSlug, authorName, ...otherProps }) { function AuthorNameLink({ titleSlug, authorName }) {
const link = `/author/${titleSlug}`; const link = `/author/${titleSlug}`;
return ( return (
<Link to={link} {...otherProps}> <Link to={link}>
{authorName} {authorName}
</Link> </Link>
); );

View File

@@ -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;
}

View File

@@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteFilesMessage': string;
'pathContainer': string;
'pathIcon': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'alternateTitle': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -44,10 +44,6 @@
margin-top: 20px; margin-top: 20px;
} }
.filterIcon {
float: right;
}
.authorNavigationButtons { .authorNavigationButtons {
position: absolute; position: absolute;
right: 0; right: 0;

View File

@@ -1,18 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'authorNavigationButton': string;
'authorNavigationButtons': string;
'authorUpButton': string;
'contentContainer': string;
'errorMessage': string;
'filterIcon': string;
'innerContentBody': string;
'metadataMessage': string;
'selectedTab': string;
'tab': string;
'tabContent': string;
'tabList': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -7,7 +7,6 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal'; import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter'; import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable'; import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import Alert from 'Components/Alert';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; 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 PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector'; 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 InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable'; import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -240,14 +239,9 @@ class AuthorDetails extends Component {
saveError, saveError,
isDeleting, isDeleting,
deleteError, deleteError,
statistics = {} statistics
} = this.props; } = this.props;
const {
bookFileCount = 0,
totalBookCount = 0
} = statistics;
const { const {
isOrganizeModalOpen, isOrganizeModalOpen,
isRetagModalOpen, isRetagModalOpen,
@@ -398,7 +392,10 @@ class AuthorDetails extends Component {
name={icons.ARROW_UP} name={icons.ARROW_UP}
size={30} size={30}
title={translate('GoToAuthorListing')} title={translate('GoToAuthorListing')}
to={'/'} to={{
pathname: '/',
state: { restoreScrollPosition: true }
}}
/> />
<IconButton <IconButton
@@ -413,25 +410,22 @@ class AuthorDetails extends Component {
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
{ {
!isPopulated && !booksError && !bookFilesError ? !isPopulated && !booksError && !bookFilesError &&
<LoadingIndicator /> : <LoadingIndicator />
null
} }
{ {
!isFetching && booksError ? !isFetching && booksError &&
<Alert kind={kinds.DANGER}> <div>
{translate('LoadingBooksFailed')} {translate('LoadingBooksFailed')}
</Alert> : </div>
null
} }
{ {
!isFetching && bookFilesError ? !isFetching && bookFilesError &&
<Alert kind={kinds.DANGER}> <div>
{translate('LoadingBookFilesFailed')} {translate('LoadingBookFilesFailed')}
</Alert> : </div>
null
} }
{ {
@@ -444,7 +438,7 @@ class AuthorDetails extends Component {
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
{translate('BooksTotal', [totalBookCount])} {translate('BooksTotal', [statistics.totalBookCount])}
</Tab> </Tab>
<Tab <Tab
@@ -472,7 +466,7 @@ class AuthorDetails extends Component {
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
{translate('FilesTotal', [bookFileCount])} {translate('FilesTotal', [statistics.bookFileCount])}
</Tab> </Tab>
{ {

View File

@@ -155,6 +155,7 @@ function createMapStateToProps() {
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing; const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id })); 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 isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR }); const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
const isRenamingAuthor = ( const isRenamingAuthor = (
isCommandExecuting(isRenamingAuthorCommand) && isCommandExecuting(isRenamingAuthorCommand) &&

View File

@@ -136,9 +136,8 @@
} }
.title { .title {
font-weight: 300;
font-size: 30px; font-size: 30px;
line-height: 30px; line-height: 50px;
} }
} }

View File

@@ -1,29 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'alternateTitlesIconContainer': string;
'authorNavigationButton': string;
'authorNavigationButtons': string;
'authorUpButton': string;
'backdrop': string;
'backdropOverlay': string;
'details': string;
'detailsLabel': string;
'header': string;
'headerContent': string;
'info': string;
'links': string;
'monitorToggleButton': string;
'overview': string;
'path': string;
'poster': string;
'qualityProfileName': string;
'sizeOnDisk': string;
'tags': string;
'title': string;
'titleContainer': string;
'titleRow': string;
'toggleMonitoredContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate'; import TextTruncate from 'react-text-truncate';
import AuthorPoster from 'Author/AuthorPoster'; import AuthorPoster from 'Author/AuthorPoster';
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
import HeartRating from 'Components/HeartRating'; import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Label from 'Components/Label'; import Label from 'Components/Label';
@@ -12,7 +11,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; 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 fonts from 'Styles/Variables/fonts';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
@@ -26,7 +25,12 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight); const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) { 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 { class AuthorDetailsHeader extends Component {
@@ -88,11 +92,10 @@ class AuthorDetailsHeader extends Component {
titleWidth titleWidth
} = this.state; } = this.state;
const statusDetails = getAuthorStatusDetails(status);
const fanartUrl = getFanartUrl(images);
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160); const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
const continuing = status === 'continuing';
let bookFilesCountMessage = translate('BookFilesCountMessage'); let bookFilesCountMessage = translate('BookFilesCountMessage');
if (bookFileCount === 1) { if (bookFileCount === 1) {
@@ -105,11 +108,9 @@ class AuthorDetailsHeader extends Component {
<div className={styles.header} style={{ width }} > <div className={styles.header} style={{ width }} >
<div <div
className={styles.backdrop} className={styles.backdrop}
style={ style={{
fanartUrl ? backgroundImage: `url(${getFanartUrl(images)})`
{ backgroundImage: `url(${fanartUrl})` } : }}
null
}
> >
<div className={styles.backdropOverlay} /> <div className={styles.backdropOverlay} />
</div> </div>
@@ -214,7 +215,7 @@ class AuthorDetailsHeader extends Component {
<span className={styles.qualityProfileName}> <span className={styles.qualityProfileName}>
{ {
<QualityProfileName <QualityProfileNameConnector
qualityProfileId={qualityProfileId} qualityProfileId={qualityProfileId}
/> />
} }
@@ -237,16 +238,16 @@ class AuthorDetailsHeader extends Component {
<Label <Label
className={styles.detailsLabel} className={styles.detailsLabel}
title={statusDetails.message} title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
size={sizes.LARGE} size={sizes.LARGE}
> >
<Icon <Icon
name={statusDetails.icon} name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
size={17} size={17}
/> />
<span className={styles.qualityProfileName}> <span className={styles.qualityProfileName}>
{statusDetails.title} {continuing ? 'Continuing' : 'Deceased'}
</span> </span>
</Label> </Label>

View File

@@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'link': string;
'linkLabel': string;
'links': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -74,7 +74,7 @@ class AuthorDetailsPageConnector extends Component {
if (isFetching && !isPopulated) { if (isFetching && !isPopulated) {
return ( return (
<PageContent title={translate('Loading')}> <PageContent title='loading'>
<PageContentBody> <PageContentBody>
<LoadingIndicator /> <LoadingIndicator />
</PageContentBody> </PageContentBody>

View File

@@ -1,23 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actionButton': string;
'actionMenuIcon': string;
'actions': string;
'actionsMenu': string;
'actionsMenuContent': string;
'bookCount': string;
'bookType': string;
'bookTypeLabel': string;
'books': string;
'collapseButtonContainer': string;
'collapseButtonIcon': string;
'episodeCountTooltip': string;
'expandButton': string;
'expandButtonIcon': string;
'header': string;
'left': string;
'noBooks': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,24 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actionButton': string;
'actionMenuIcon': string;
'actions': string;
'actionsMenu': string;
'actionsMenuContent': string;
'bookCount': string;
'bookType': string;
'bookTypeLabel': string;
'books': string;
'collapseButtonContainer': string;
'collapseButtonIcon': string;
'episodeCountTooltip': string;
'expandButton': string;
'expandButtonIcon': string;
'header': string;
'left': string;
'noBooks': string;
'seriesTitle': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector'; import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
@@ -9,11 +10,15 @@ function createMapStateToProps() {
createAuthorSelector(), createAuthorSelector(),
createTagsSelector(), createTagsSelector(),
(author, tagList) => { (author, tagList) => {
const tags = author.tags const tags = _.reduce(author.tags, (acc, tag) => {
.map((tagId) => tagList.find((tag) => tag.id === tagId)) const matchingTag = _.find(tagList, { id: tag });
.filter((tag) => !!tag)
.map((tag) => tag.label) if (matchingTag) {
.sort((a, b) => a.localeCompare(b)); acc.push(matchingTag.label);
}
return acc;
}, []);
return { return {
tags tags

View File

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

View File

@@ -1,14 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'indexerFlags': string;
'monitored': string;
'pageCount': string;
'position': string;
'rating': string;
'releaseDate': string;
'status': string;
'title': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -2,17 +2,12 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import BookSearchCellConnector from 'Book/BookSearchCellConnector'; import BookSearchCellConnector from 'Book/BookSearchCellConnector';
import BookTitleLink from 'Book/BookTitleLink'; import BookTitleLink from 'Book/BookTitleLink';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating'; import StarRating from 'Components/StarRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; 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 BookStatus from './BookStatus';
import styles from './BookRow.css'; import styles from './BookRow.css';
@@ -64,7 +59,6 @@ class BookRow extends Component {
releaseDate, releaseDate,
title, title,
seriesTitle, seriesTitle,
authorName,
position, position,
pageCount, pageCount,
ratings, ratings,
@@ -72,7 +66,6 @@ class BookRow extends Component {
authorMonitored, authorMonitored,
titleSlug, titleSlug,
bookFiles, bookFiles,
indexerFlags,
isEditorActive, isEditorActive,
isSelected, isSelected,
onSelectedChange, 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') { if (name === 'status') {
return ( return (
<TableRowCell <TableRowCell
@@ -236,7 +211,6 @@ class BookRow extends Component {
bookId={id} bookId={id}
authorId={authorId} authorId={authorId}
bookTitle={title} bookTitle={title}
authorName={authorName}
/> />
); );
} }
@@ -255,11 +229,9 @@ BookRow.propTypes = {
releaseDate: PropTypes.string, releaseDate: PropTypes.string,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
seriesTitle: PropTypes.string.isRequired, seriesTitle: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
position: PropTypes.string, position: PropTypes.string,
pageCount: PropTypes.number, pageCount: PropTypes.number,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
indexerFlags: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool, isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired, authorMonitored: PropTypes.bool.isRequired,
@@ -271,8 +243,4 @@ BookRow.propTypes = {
onMonitorBookPress: PropTypes.func.isRequired onMonitorBookPress: PropTypes.func.isRequired
}; };
BookRow.defaultProps = {
indexerFlags: 0
};
export default BookRow; export default BookRow;

View File

@@ -7,18 +7,21 @@ import BookRow from './BookRow';
const selectBookFiles = createSelector( const selectBookFiles = createSelector(
(state) => state.bookFiles, (state) => state.bookFiles,
(bookFiles) => { (bookFiles) => {
const { items } = bookFiles; const {
items
} = bookFiles;
return items.reduce((acc, file) => { const bookFileDict = items.reduce((acc, file) => {
const bookId = file.bookId; const bookId = file.bookId;
if (!acc.hasOwnProperty(bookId)) { if (!acc.hasOwnProperty(bookId)) {
acc[bookId] = []; acc[bookId] = [];
} }
acc[bookId].push(file); acc[bookId].push(file);
return acc; return acc;
}, {}); }, {});
return bookFileDict;
} }
); );
@@ -28,14 +31,9 @@ function createMapStateToProps() {
selectBookFiles, selectBookFiles,
(state, { id }) => id, (state, { id }) => id,
(author = {}, bookFiles, bookId) => { (author = {}, bookFiles, bookId) => {
const files = bookFiles[bookId] ?? [];
const bookFile = files[0];
return { return {
authorMonitored: author.monitored, authorMonitored: author.monitored,
authorName: author.authorName, bookFiles: bookFiles[bookId] ?? []
bookFiles: files,
indexerFlags: bookFile ? bookFile.indexerFlags : 0
}; };
} }
); );

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'center': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,8 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteButton': string;
'labelIcon': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,12 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
'retagIcon': string;
'searchForNewBookContainer': string;
'searchForNewBookInput': string;
'searchForNewBookLabel': string;
'searchForNewBookLabelContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,16 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'buttonContainer': string;
'buttonContainerContent': string;
'buttons': string;
'deleteSelectedButton': string;
'dropdownContainer': string;
'footer': string;
'inputContainer': string;
'organizeSelectedButton': string;
'selectedAuthorLabel': string;
'tagsButton': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal'; import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput'; import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
@@ -10,7 +9,6 @@ import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/Settings/rootFolders';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel'; import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
import DeleteAuthorModal from './Delete/DeleteAuthorModal'; import DeleteAuthorModal from './Delete/DeleteAuthorModal';
@@ -19,10 +17,6 @@ import styles from './AuthorEditorFooter.css';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const mapDispatchToProps = {
dispatchFetchRootFolders: fetchRootFolders
};
class AuthorEditorFooter extends Component { class AuthorEditorFooter extends Component {
// //
@@ -45,13 +39,6 @@ class AuthorEditorFooter extends Component {
}; };
} }
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
isSaving, isSaving,
@@ -173,9 +160,9 @@ class AuthorEditorFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true }, { key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'monitored', value: translate('Monitored') }, { key: 'monitored', value: 'Monitored' },
{ key: 'unmonitored', value: translate('Unmonitored') } { key: 'unmonitored', value: 'Unmonitored' }
]; ];
return ( return (
@@ -354,8 +341,7 @@ AuthorEditorFooter.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired, onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired, onRetagAuthorPress: PropTypes.func.isRequired
dispatchFetchRootFolders: PropTypes.func.isRequired
}; };
export default connect(undefined, mapDispatchToProps)(AuthorEditorFooter); export default AuthorEditorFooter;

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