Compare commits

..

1 Commits

Author SHA1 Message Date
Mark McDowall 03ed01e7d8 New: Setting to disable authentication for local addresses
(cherry picked from commit b154b00c6156512e7fbd0a2c4833c116a74f23ca)
2023-08-09 14:46:01 +03:00
538 changed files with 5232 additions and 10819 deletions
+2 -1
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
+1 -1
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
+3
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.
-16
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).
-17
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'
+32
View File
@@ -0,0 +1,32 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v3
with:
github-token: ${{ github.token }}
support-label: 'Type: Support'
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord)
or [Subreddit](https://reddit.com/r/readarr)
close-issue: true
lock-issue: false
- uses: dessant/support-requests@v3
with:
github-token: ${{ github.token }}
support-label: 'Status: Logs Needed'
issue-comment: >
:wave: @{issue-author}, In order to help you further we'll need to see logs.
You'll need to enable trace logging and replicate the problem that you encountered.
Guidance on how to enable trace logging can be found in
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
close-issue: false
lock-issue: false
+1
View File
@@ -30,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
+141 -294
View File
@@ -9,13 +9,13 @@ 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.3.18' majorVersion: '0.3.2'
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.417' dotnetVersion: '6.0.408'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
@@ -27,10 +27,6 @@ trigger:
include: include:
- develop - develop
- master - master
paths:
exclude:
- .github
- src/Readarr.Api.*/openapi.json
pr: pr:
branches: branches:
@@ -38,37 +34,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 +122,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 +137,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 +160,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:
@@ -178,6 +192,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 +204,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 +231,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 +240,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
@@ -391,29 +406,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 +479,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 +492,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 +530,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 +545,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 +556,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 +579,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 +608,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 +630,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 +649,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 +682,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 +705,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 +755,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 +773,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 +801,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 +823,7 @@ stages:
- stage: Automation - stage: Automation
displayName: Automation displayName: Automation
dependsOn: Packages dependsOn: Packages
jobs: jobs:
- job: Automation - job: Automation
strategy: strategy:
@@ -975,23 +833,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 +868,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 +888,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:
@@ -1087,6 +927,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
@@ -1115,16 +956,11 @@ stages:
cliProjectVersion: '$(readarrVersion)' cliProjectVersion: '$(readarrVersion)'
cliSources: './frontend' cliSources: './frontend'
- task: SonarCloudAnalyze@1 - task: SonarCloudAnalyze@1
- job: Api_Docs - job: Api_Docs
displayName: API Docs displayName: API Docs
dependsOn: Prepare
condition: | condition: |
and and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
(
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
)
pool: pool:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.windowsImage }}
@@ -1137,7 +973,7 @@ stages:
- checkout: self - checkout: self
submodules: true submodules: true
persistCredentials: true persistCredentials: true
fetchDepth: 1 fetchDepth: 1
- bash: ./docs.sh Windows - bash: ./docs.sh Windows
displayName: Create openapi.json displayName: Create openapi.json
- bash: | - bash: |
@@ -1145,9 +981,10 @@ stages:
git config --global user.name "Servarr" git config --global user.name "Servarr"
git checkout -b api-docs git checkout -b api-docs
git add . git add .
if git status | grep -q modified git status
if git status | grep modified
then then
git commit -am 'Automated API Docs update' git commit -am 'Automated API Docs update [skip ci]'
git push -f --set-upstream origin api-docs 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"}' 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 else
@@ -1171,25 +1008,33 @@ stages:
- 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:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- task: SonarCloudPrepare@1 - task: SonarCloudPrepare@1
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs: inputs:
@@ -1200,14 +1045,16 @@ stages:
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
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- task: SonarCloudAnalyze@1 - 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
@@ -1230,6 +1077,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:
@@ -1253,4 +1101,3 @@ stages:
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId) DISCORDTHREADID: $(discordThreadId)
+3 -3
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()
+4 -6
View File
@@ -2,18 +2,16 @@ 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: {
+2 -3
View File
@@ -36,7 +36,7 @@ module.exports = (env) => {
}, },
entry: { entry: {
index: 'index.ts' index: 'index.js'
}, },
resolve: { resolve: {
@@ -98,8 +98,7 @@ module.exports = (env) => {
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({
+3 -14
View File
@@ -23,7 +23,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 {
@@ -289,16 +289,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 +299,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 +338,4 @@ Queue.propTypes = {
onRemoveSelectedPress: PropTypes.func.isRequired onRemoveSelectedPress: PropTypes.func.isRequired
}; };
Queue.defaultProps = {
count: 0
};
export default Queue; export default Queue;
-3
View File
@@ -98,7 +98,6 @@ class QueueRow extends Component {
indexer, indexer,
outputPath, outputPath,
downloadClient, downloadClient,
downloadClientHasPostImportCategory,
downloadForced, downloadForced,
estimatedCompletionTime, estimatedCompletionTime,
timeleft, timeleft,
@@ -390,7 +389,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}
@@ -420,7 +418,6 @@ QueueRow.propTypes = {
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,
@@ -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('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
Close
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
Remove
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemModal;
@@ -1,230 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css';
interface RemovePressProps {
remove: boolean;
changeCategory: boolean;
blocklist: boolean;
skipRedownload: boolean;
}
interface RemoveQueueItemModalProps {
isOpen: boolean;
sourceTitle: string;
canChangeCategory: boolean;
canIgnore: boolean;
isPending: boolean;
selectedCount?: number;
onRemovePress(props: RemovePressProps): void;
onModalClose: () => void;
}
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
sourceTitle,
canIgnore,
canChangeCategory,
isPending,
selectedCount,
onRemovePress,
onModalClose,
} = props;
const multipleSelected = selectedCount && selectedCount > 1;
const [removalMethod, setRemovalMethod] =
useState<RemovalMethod>('removeFromClient');
const [blocklistMethod, setBlocklistMethod] =
useState<BlocklistMethod>('doNotBlocklist');
const { title, message } = useMemo(() => {
if (!selectedCount) {
return {
title: translate('RemoveQueueItem', { sourceTitle }),
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
};
}
if (selectedCount === 1) {
return {
title: translate('RemoveSelectedItem'),
message: translate('RemoveSelectedItemQueueMessageText'),
};
}
return {
title: translate('RemoveSelectedItems'),
message: translate('RemoveSelectedItemsQueueMessageText', {
selectedCount,
}),
};
}, [sourceTitle, selectedCount]);
const removalMethodOptions = useMemo(() => {
return [
{
key: 'removeFromClient',
value: translate('RemoveFromDownloadClient'),
hint: multipleSelected
? translate('RemoveMultipleFromDownloadClientHint')
: translate('RemoveFromDownloadClientHint'),
},
{
key: 'changeCategory',
value: translate('ChangeCategory'),
isDisabled: !canChangeCategory,
hint: multipleSelected
? translate('ChangeCategoryMultipleHint')
: translate('ChangeCategoryHint'),
},
{
key: 'ignore',
value: multipleSelected
? translate('IgnoreDownloads')
: translate('IgnoreDownload'),
isDisabled: !canIgnore,
hint: multipleSelected
? translate('IgnoreDownloadsHint')
: translate('IgnoreDownloadHint'),
},
];
}, [canChangeCategory, canIgnore, multipleSelected]);
const blocklistMethodOptions = useMemo(() => {
return [
{
key: 'doNotBlocklist',
value: translate('DoNotBlocklist'),
hint: translate('DoNotBlocklistHint'),
},
{
key: 'blocklistAndSearch',
value: translate('BlocklistAndSearch'),
hint: multipleSelected
? translate('BlocklistAndSearchMultipleHint')
: translate('BlocklistAndSearchHint'),
},
{
key: 'blocklistOnly',
value: translate('BlocklistOnly'),
hint: multipleSelected
? translate('BlocklistMultipleOnlyHint')
: translate('BlocklistOnlyHint'),
},
];
}, [multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {
setRemovalMethod(value);
},
[setRemovalMethod]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
);
const handleConfirmRemove = useCallback(() => {
onRemovePress({
remove: removalMethod === 'removeFromClient',
changeCategory: removalMethod === 'changeCategory',
blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly',
});
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose();
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
<ModalContent onModalClose={handleModalClose}>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<div className={styles.message}>{message}</div>
{isPending ? null : (
<FormGroup>
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="removalMethod"
value={removalMethod}
values={removalMethodOptions}
isDisabled={!canChangeCategory && !canIgnore}
helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning'
)}
onChange={handleRemovalMethodChange}
/>
</FormGroup>
)}
<FormGroup>
<FormLabel>
{multipleSelected
? translate('BlocklistReleases')
: translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="blocklistMethod"
value={blocklistMethod}
values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')}
onChange={handleBlocklistMethodChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={handleModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default RemoveQueueItemModal;
@@ -0,0 +1,178 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemsModal.css';
class RemoveQueueItemsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blocklist: false,
skipRedownload: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipRedownload: false
});
};
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
//
// Render
render() {
const {
isOpen,
selectedCount,
canIgnore,
allPending
} = this.props;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemsModal;
+4 -3
View File
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme'; import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
function App({ store, history }) { function App({ store, history, hasTranslationsError }) {
return ( return (
<DocumentTitle title={window.Readarr.instanceName}> <DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ApplyTheme> <ApplyTheme>
<PageConnector> <PageConnector hasTranslationsError={hasTranslationsError}>
<AppRoutes app={App} /> <AppRoutes app={App} />
</PageConnector> </PageConnector>
</ApplyTheme> </ApplyTheme>
@@ -25,7 +25,8 @@ function App({ store, history }) {
App.propTypes = { App.propTypes = {
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired history: PropTypes.object.isRequired,
hasTranslationsError: PropTypes.bool.isRequired
}; };
export default App; export default App;
@@ -1,7 +1,6 @@
.version { .version {
margin: 0 3px; margin: 0 3px;
font-weight: bold; font-weight: bold;
font-family: var(--defaultFontFamily);
} }
.maintenance { .maintenance {
+8 -7
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>
+4 -5
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>
+6 -3
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;
} }
} }
@@ -44,10 +44,6 @@
margin-top: 20px; margin-top: 20px;
} }
.filterIcon {
float: right;
}
.authorNavigationButtons { .authorNavigationButtons {
position: absolute; position: absolute;
right: 0; right: 0;
-1
View File
@@ -6,7 +6,6 @@ interface CssExports {
'authorUpButton': string; 'authorUpButton': string;
'contentContainer': string; 'contentContainer': string;
'errorMessage': string; 'errorMessage': string;
'filterIcon': string;
'innerContentBody': string; 'innerContentBody': string;
'metadataMessage': string; 'metadataMessage': string;
'selectedTab': string; 'selectedTab': string;
+3 -8
View File
@@ -239,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,
@@ -440,7 +435,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
@@ -468,7 +463,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>
{ {
@@ -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) &&
@@ -136,9 +136,8 @@
} }
.title { .title {
font-weight: 300;
font-size: 30px; font-size: 30px;
line-height: 30px; line-height: 50px;
} }
} }
@@ -25,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 {
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector'; import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
import AuthorHistoryModalContent from './AuthorHistoryModalContent'; import AuthorHistoryModalContent from './AuthorHistoryModalContent';
@@ -15,7 +14,6 @@ function AuthorHistoryModal(props) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<AuthorHistoryContentConnector <AuthorHistoryContentConnector
@@ -5,7 +5,6 @@ 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';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate';
import AuthorHistoryTableContent from './AuthorHistoryTableContent'; import AuthorHistoryTableContent from './AuthorHistoryTableContent';
class AuthorHistoryModalContent extends Component { class AuthorHistoryModalContent extends Component {
@@ -21,7 +20,7 @@ class AuthorHistoryModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('History')} History
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -32,7 +31,7 @@ class AuthorHistoryModalContent extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}> <Button onPress={onModalClose}>
{translate('Close')} Close
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -4,6 +4,7 @@
word-break: break-word; word-break: break-word;
} }
.details,
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'details': string;
'sourceTitle': string; 'sourceTitle': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality'; import BookQuality from 'Book/BookQuality';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
@@ -12,7 +11,6 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
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 { icons, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AuthorHistoryRow.css'; import styles from './AuthorHistoryRow.css';
@@ -77,8 +75,6 @@ class AuthorHistoryRow extends Component {
sourceTitle, sourceTitle,
quality, quality,
qualityCutoffNotMet, qualityCutoffNotMet,
customFormats,
customFormatScore,
date, date,
data, data,
book book
@@ -110,19 +106,11 @@ class AuthorHistoryRow extends Component {
/> />
</TableRowCell> </TableRowCell>
<TableRowCell>
<BookFormats formats={customFormats} />
</TableRowCell>
<TableRowCell>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCellConnector <RelativeDateCellConnector
date={date} date={date}
/> />
<TableRowCell className={styles.actions}> <TableRowCell className={styles.details}>
<Popover <Popover
anchor={ anchor={
<Icon <Icon
@@ -139,13 +127,14 @@ class AuthorHistoryRow extends Component {
} }
position={tooltipPositions.LEFT} position={tooltipPositions.LEFT}
/> />
</TableRowCell>
<TableRowCell className={styles.actions}>
{ {
eventType === 'grabbed' && eventType === 'grabbed' &&
<IconButton <IconButton
title={translate('MarkAsFailed')} title={translate('MarkAsFailed')}
name={icons.REMOVE} name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress} onPress={this.onMarkAsFailedPress}
/> />
} }
@@ -171,8 +160,6 @@ AuthorHistoryRow.propTypes = {
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
fullAuthor: PropTypes.bool.isRequired, fullAuthor: PropTypes.bool.isRequired,
@@ -1,9 +0,0 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}
@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'container': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector'; import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent'; import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
import styles from './AuthorHistoryTable.css';
function AuthorHistoryTable(props) { function AuthorHistoryTable(props) {
const { const {
@@ -9,12 +8,10 @@ function AuthorHistoryTable(props) {
} = props; } = props;
return ( return (
<div className={styles.container}> <AuthorHistoryContentConnector
<AuthorHistoryContentConnector component={AuthorHistoryTableContent}
component={AuthorHistoryTableContent} {...otherProps}
{...otherProps} />
/>
</div>
); );
} }
@@ -1,5 +0,0 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'blankpad': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,14 +1,12 @@
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 Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector'; import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
import styles from './AuthorHistoryTableContent.css';
const columns = [ const columns = [
{ {
@@ -17,41 +15,32 @@ const columns = [
}, },
{ {
name: 'book', name: 'book',
label: () => translate('Book'), label: 'Book',
isVisible: true isVisible: true
}, },
{ {
name: 'sourceTitle', name: 'sourceTitle',
label: () => translate( 'SourceTitle'), label: 'Source Title',
isVisible: true isVisible: true
}, },
{ {
name: 'quality', name: 'quality',
label: () => translate('Quality'), label: 'Quality',
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'date', name: 'date',
label: () => translate('Date'), label: 'Date',
isVisible: true
},
{
name: 'details',
label: 'Details',
isVisible: true isVisible: true
}, },
{ {
name: 'actions', name: 'actions',
label: 'Actions',
isVisible: true isVisible: true
} }
]; ];
@@ -75,7 +64,7 @@ class AuthorHistoryTableContent extends Component {
const hasItems = !!items.length; const hasItems = !!items.length;
return ( return (
<div> <>
{ {
isFetching && isFetching &&
<LoadingIndicator /> <LoadingIndicator />
@@ -90,7 +79,7 @@ class AuthorHistoryTableContent extends Component {
{ {
isPopulated && !hasItems && !error && isPopulated && !hasItems && !error &&
<div className={styles.blankpad}> <div>
{translate('NoHistory')} {translate('NoHistory')}
</div> </div>
} }
@@ -114,7 +103,7 @@ class AuthorHistoryTableContent extends Component {
</TableBody> </TableBody>
</Table> </Table>
} }
</div> </>
); );
} }
} }
@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createAuthorClientSideCollectionItemsSelector('authorIndex'), createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR), createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR), createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
@@ -24,17 +24,17 @@ function createMapStateToProps() {
( (
author, author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState dimensionsState
) => { ) => {
return { return {
...author, ...author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
@@ -14,39 +14,14 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const nameOptions = [ const nameOptions = [
{ { key: 'firstLast', value: translate('NameFirstLast') },
key: 'firstLast', { key: 'lastFirst', value: translate('NameLastFirst') }
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
]; ];
const posterSizeOptions = [ const posterSizeOptions = [
{ { key: 'small', value: 'Small' },
key: 'small', { key: 'medium', value: 'Medium' },
get value() { { key: 'large', value: 'Large' }
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
]; ];
class AuthorIndexOverviewOptionsModalContent extends Component { class AuthorIndexOverviewOptionsModalContent extends Component {
@@ -14,45 +14,15 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const posterSizeOptions = [ const posterSizeOptions = [
{ { key: 'small', value: 'Small' },
key: 'small', { key: 'medium', value: 'Medium' },
get value() { { key: 'large', value: 'Large' }
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
]; ];
const nameOptions = [ const nameOptions = [
{ { key: 'no', value: translate('NoName') },
key: 'no', { key: 'firstLast', value: translate('NameFirstLast') },
get value() { { key: 'lastFirst', value: translate('NameLastFirst') }
return translate('NoName');
}
},
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
]; ];
class AuthorIndexPosterOptionsModalContent extends Component { class AuthorIndexPosterOptionsModalContent extends Component {
@@ -7,18 +7,8 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const nameOptions = [ const nameOptions = [
{ { key: 'firstLast', value: translate('NameFirstLast') },
key: 'firstLast', { key: 'lastFirst', value: translate('NameLastFirst') }
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
]; ];
class AuthorIndexTableOptions extends Component { class AuthorIndexTableOptions extends Component {
+10 -46
View File
@@ -3,7 +3,6 @@ import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) { function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
const revision = quality.revision; const revision = quality.revision;
@@ -29,36 +28,6 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
return title; return title;
} }
function revisionLabel(className, quality, showRevision) {
if (!showRevision) {
return;
}
if (quality.revision.isRepack) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Repack')}
>
R
</Label>
);
}
if (quality.revision.version && quality.revision.version > 1) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Proper')}
>
P
</Label>
);
}
}
function BookQuality(props) { function BookQuality(props) {
const { const {
className, className,
@@ -66,8 +35,7 @@ function BookQuality(props) {
quality, quality,
size, size,
isMonitored, isMonitored,
isCutoffNotMet, isCutoffNotMet
showRevision
} = props; } = props;
let kind = kinds.DEFAULT; let kind = kinds.DEFAULT;
@@ -82,15 +50,13 @@ function BookQuality(props) {
} }
return ( return (
<span> <Label
<Label className={className}
className={className} kind={kind}
kind={kind} title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)} >
> {quality.quality.name}
{quality.quality.name} </Label>
</Label>{revisionLabel(className, quality, showRevision)}
</span>
); );
} }
@@ -100,14 +66,12 @@ BookQuality.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
size: PropTypes.number, size: PropTypes.number,
isMonitored: PropTypes.bool, isMonitored: PropTypes.bool,
isCutoffNotMet: PropTypes.bool, isCutoffNotMet: PropTypes.bool
showRevision: PropTypes.bool
}; };
BookQuality.defaultProps = { BookQuality.defaultProps = {
title: '', title: '',
isMonitored: true, isMonitored: true
showRevision: false
}; };
export default BookQuality; export default BookQuality;
+4 -10
View File
@@ -99,14 +99,9 @@ class BookDetails extends Component {
nextBook, nextBook,
isSearching, isSearching,
onRefreshPress, onRefreshPress,
onSearchPress, onSearchPress
statistics = {}
} = this.props; } = this.props;
const {
bookFileCount = 0
} = statistics;
const { const {
isOrganizeModalOpen, isOrganizeModalOpen,
isRetagModalOpen, isRetagModalOpen,
@@ -243,21 +238,21 @@ class BookDetails extends Component {
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
{translate('History')} History
</Tab> </Tab>
<Tab <Tab
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
{translate('Search')} Search
</Tab> </Tab>
<Tab <Tab
className={styles.tab} className={styles.tab}
selectedClassName={styles.selectedTab} selectedClassName={styles.selectedTab}
> >
{translate('FilesTotal', [bookFileCount])} Files
</Tab> </Tab>
{ {
@@ -340,7 +335,6 @@ BookDetails.propTypes = {
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired, links: PropTypes.arrayOf(PropTypes.object).isRequired,
statistics: PropTypes.object.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
@@ -69,21 +69,16 @@ function createMapStateToProps() {
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks); const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks); const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
const isRefreshing = (
isCommandExecuting(isRefreshingCommand) &&
isRefreshingCommand.body.bookId === book.id
);
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH }); const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
const isSearching = ( const isSearching = (
isCommandExecuting(isSearchingCommand) && isCommandExecuting(isSearchingCommand) &&
isSearchingCommand.body.bookIds.indexOf(book.id) > -1 isSearchingCommand.body.bookIds.indexOf(book.id) > -1
); );
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR }); const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
const isRenamingAuthor = ( const isRefreshing = (
isCommandExecuting(isRenamingAuthorCommand) && isCommandExecuting(isRefreshingCommand) &&
isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1 isRefreshingCommand.body.bookId === book.id
); );
const isFetching = isBookFilesFetching || editions.isFetching; const isFetching = isBookFilesFetching || editions.isFetching;
@@ -95,8 +90,6 @@ function createMapStateToProps() {
author, author,
isRefreshing, isRefreshing,
isSearching, isSearching,
isRenamingFiles,
isRenamingAuthor,
isFetching, isFetching,
isPopulated, isPopulated,
bookFilesError, bookFilesError,
@@ -132,27 +125,9 @@ class BookDetailsConnector extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { if (prevProps.id !== this.props.id ||
id,
anyReleaseOk,
isRenamingFiles,
isRenamingAuthor
} = this.props;
if (
(prevProps.isRenamingFiles && !isRenamingFiles) ||
(prevProps.isRenamingAuthor && !isRenamingAuthor) ||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) || !_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && anyReleaseOk === true) (prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
) {
this.unpopulate();
this.populate();
}
// If the id has changed we need to clear the book
// files and fetch from the server.
if (prevProps.id !== id) {
this.unpopulate(); this.unpopulate();
this.populate(); this.populate();
} }
@@ -222,8 +197,6 @@ class BookDetailsConnector extends Component {
BookDetailsConnector.propTypes = { BookDetailsConnector.propTypes = {
id: PropTypes.number, id: PropTypes.number,
anyReleaseOk: PropTypes.bool, anyReleaseOk: PropTypes.bool,
isRenamingFiles: PropTypes.bool.isRequired,
isRenamingAuthor: PropTypes.bool.isRequired,
isBookFetching: PropTypes.bool, isBookFetching: PropTypes.bool,
isBookPopulated: PropTypes.bool, isBookPopulated: PropTypes.bool,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
@@ -117,9 +117,8 @@
} }
.title { .title {
font-weight: 300;
font-size: 30px; font-size: 30px;
line-height: 30px; line-height: 50px;
} }
} }
@@ -21,7 +21,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 BookDetailsHeader extends Component { class BookDetailsHeader extends Component {
@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookClientSideCollectionItemsSelector('bookIndex'), createBookClientSideCollectionItemsSelector('bookIndex'),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK), createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH), createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH), createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
@@ -229,6 +229,7 @@ class BookIndexRow extends Component {
className={styles[name]} className={styles[name]}
> >
{bookFileCount} {bookFileCount}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -1,9 +0,0 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}
@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'container': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector'; import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
import styles from './BookFileEditorTable.css';
function BookFileEditorTable(props) { function BookFileEditorTable(props) {
const { const {
@@ -8,11 +7,9 @@ function BookFileEditorTable(props) {
} = props; } = props;
return ( return (
<div className={styles.container}> <BookFileEditorTableContentConnector
<BookFileEditorTableContentConnector {...otherProps}
{...otherProps} />
/>
</div>
); );
} }
@@ -1,6 +1,6 @@
.filesTable { .filesTable {
margin: 10px; margin-bottom: 20px;
padding-top: 5px; padding-top: 15px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-top: 1px solid var(--borderColor); border-top: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
@@ -13,15 +13,9 @@
.actions { .actions {
display: flex; display: flex;
margin: 10px; margin-right: auto;
} }
.selectInput { .selectInput {
margin-left: 10px; margin-left: 10px;
} }
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
@@ -2,7 +2,6 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'blankpad': string;
'filesTable': string; 'filesTable': string;
'selectInput': string; 'selectInput': string;
} }
@@ -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 SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -121,7 +120,7 @@ class BookFileEditorTableContent extends Component {
const hasSelectedFiles = this.getSelectedIds().length > 0; const hasSelectedFiles = this.getSelectedIds().length > 0;
return ( return (
<div> <>
{ {
isFetching && !isPopulated ? isFetching && !isPopulated ?
<LoadingIndicator /> : <LoadingIndicator /> :
@@ -130,13 +129,13 @@ class BookFileEditorTableContent extends Component {
{ {
!isFetching && error ? !isFetching && error ?
<Alert kind={kinds.DANGER}>{error}</Alert> : <div>{error}</div> :
null null
} }
{ {
isPopulated && !items.length ? isPopulated && !items.length ?
<div className={styles.blankpad}> <div>
No book files to manage. No book files to manage.
</div> : </div> :
null null
@@ -174,30 +173,26 @@ class BookFileEditorTableContent extends Component {
null null
} }
{ <div className={styles.actions}>
isPopulated && items.length ? ( <SpinnerButton
<div className={styles.actions}> kind={kinds.DANGER}
<SpinnerButton isSpinning={isDeleting}
kind={kinds.DANGER} isDisabled={!hasSelectedFiles}
isSpinning={isDeleting} onPress={this.onDeletePress}
isDisabled={!hasSelectedFiles} >
onPress={this.onDeletePress} Delete
> </SpinnerButton>
{translate('Delete')}
</SpinnerButton>
<div className={styles.selectInput}> <div className={styles.selectInput}>
<SelectInput <SelectInput
name="quality" name="quality"
value="selectQuality" value="selectQuality"
values={qualityOptions} values={qualityOptions}
isDisabled={!hasSelectedFiles} isDisabled={!hasSelectedFiles}
onChange={this.onQualityChange} onChange={this.onQualityChange}
/> />
</div> </div>
</div> </div>
) : null
}
<ConfirmModal <ConfirmModal
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
@@ -208,7 +203,7 @@ class BookFileEditorTableContent extends Component {
onConfirm={this.onConfirmDelete} onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}
/> />
</div> </>
); );
} }
} }
+1 -1
View File
@@ -143,7 +143,7 @@ class BookshelfFooter extends Component {
<div> <div>
<div className={styles.label}> <div className={styles.label}>
{translate('CountAuthorsSelected', { selectedCount })} {selectedCount} Author(s) Selected
</div> </div>
<SpinnerButton <SpinnerButton
+1 -1
View File
@@ -47,7 +47,7 @@ class CalendarConnector extends Component {
gotoCalendarToday gotoCalendarToday
} = this.props; } = this.props;
registerPagePopulator(this.repopulate, ['bookFileUpdated', 'bookFileDeleted']); registerPagePopulator(this.repopulate);
if (useCurrentPage) { if (useCurrentPage) {
fetchCalendar(); fetchCalendar();
@@ -1,7 +1,9 @@
.description {
line-height: $lineHeight;
}
.description { .description {
margin-left: 0; margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@@ -25,10 +25,6 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.version {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointMedium) {
.image { .image {
height: 250px; height: 250px;
@@ -6,7 +6,6 @@ interface CssExports {
'image': string; 'image': string;
'imageContainer': string; 'imageContainer': string;
'message': string; 'message': string;
'version': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './ErrorBoundaryError.css';
function ErrorBoundaryError(props) {
const {
className,
messageClassName,
detailsClassName,
message,
error,
info
} = props;
return (
<div className={className}>
<div className={messageClassName}>
{message}
</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{
error &&
<div>
{error.toString()}
</div>
}
<div className={styles.info}>
{info.componentStack}
</div>
</details>
</div>
);
}
ErrorBoundaryError.propTypes = {
className: PropTypes.string.isRequired,
messageClassName: PropTypes.string.isRequired,
detailsClassName: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
error: PropTypes.object.isRequired,
info: PropTypes.object.isRequired
};
ErrorBoundaryError.defaultProps = {
className: styles.container,
messageClassName: styles.message,
detailsClassName: styles.details,
message: 'There was an error loading this content'
};
export default ErrorBoundaryError;
@@ -1,77 +0,0 @@
import React, { useEffect, useState } from 'react';
import StackTrace from 'stacktrace-js';
import translate from 'Utilities/String/translate';
import styles from './ErrorBoundaryError.css';
interface ErrorBoundaryErrorProps {
className: string;
messageClassName: string;
detailsClassName: string;
message: string;
error: Error;
info: {
componentStack: string;
};
}
function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
const {
className = styles.container,
messageClassName = styles.message,
detailsClassName = styles.details,
message = translate('ErrorLoadingContent'),
error,
info,
} = props;
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {
StackTrace.fromError(error).then((de) => {
setDetailedError(de);
});
} else {
setDetailedError(null);
}
}, [error, setDetailedError]);
return (
<div className={className}>
<div className={messageClassName}>{message}</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{error ? <div>{error.message}</div> : null}
{detailedError ? (
detailedError.map((d, index) => {
return (
<div key={index}>
{` at ${d.functionName} (${d.fileName}:${d.lineNumber}:${d.columnNumber})`}
</div>
);
})
) : (
<div>{info.componentStack}</div>
)}
{
<div className={styles.version}>
Version: {window.Readarr.version}
</div>
}
</details>
</div>
);
}
export default ErrorBoundaryError;
@@ -206,11 +206,9 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp; const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => { const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return { return {
key: name, key: availablePropFilter.name,
value: typeof label === 'function' ? label() : label value: availablePropFilter.label
}; };
}).sort((a, b) => a.value.localeCompare(b.value)); }).sort((a, b) => a.value.localeCompare(b.value));
@@ -29,24 +29,22 @@ function CustomFiltersModalContent(props) {
<ModalBody> <ModalBody>
{ {
customFilters customFilters.map((customFilter) => {
.sort((a, b) => a.label.localeCompare(b.label)) return (
.map((customFilter) => { <CustomFilter
return ( key={customFilter.id}
<CustomFilter id={customFilter.id}
key={customFilter.id} label={customFilter.label}
id={customFilter.id} filters={customFilter.filters}
label={customFilter.label} selectedFilterKey={selectedFilterKey}
filters={customFilter.filters} isDeleting={isDeleting}
selectedFilterKey={selectedFilterKey} deleteError={deleteError}
isDeleting={isDeleting} dispatchSetFilter={dispatchSetFilter}
deleteError={deleteError} dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
dispatchSetFilter={dispatchSetFilter} onEditPress={onEditCustomFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter} />
onEditPress={onEditCustomFilter} );
/> })
);
})
} }
<div className={styles.addButtonContainer}> <div className={styles.addButtonContainer}>
@@ -9,10 +9,6 @@
&:hover { &:hover {
background-color: var(--inputHoverBackgroundColor); background-color: var(--inputHoverBackgroundColor);
} }
&.isDisabled {
cursor: not-allowed;
}
} }
.optionCheck { .optionCheck {
@@ -273,7 +273,6 @@ FormInputGroup.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.any, value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any), values: PropTypes.arrayOf(PropTypes.any),
isDisabled: PropTypes.bool,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all), kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number, min: PropTypes.number,
+1 -3
View File
@@ -2,10 +2,8 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-right: $formLabelRightMarginWidth; margin-right: $formLabelRightMarginWidth;
padding-top: 8px;
min-height: 35px;
text-align: end;
font-weight: bold; font-weight: bold;
line-height: 35px;
} }
.hasError { .hasError {
+1 -1
View File
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { value } = this.props; const { value } = this.props;
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) { if (value !== prevProps.value && !this.state.isFocused) {
this.setState({ this.setState({
value: value == null ? '' : value.toString() value: value == null ? '' : value.toString()
}); });
+2 -2
View File
@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key} value={key}
{...otherOptionProps} {...otherOptionProps}
> >
{typeof optionValue === 'function' ? optionValue() : optionValue} {optionValue}
</option> </option>
); );
}) })
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabledClassName: PropTypes.string, disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
hasError: PropTypes.bool, hasError: PropTypes.bool,
+2 -2
View File
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return ( return (
<span <span
className={containerClassName} className={containerClassName}
title={typeof title === 'function' ? title() : title} title={title}
> >
{icon} {icon}
</span> </span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired, name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired, kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), title: PropTypes.string,
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired fixedWidth: PropTypes.bool.isRequired
}; };
@@ -97,7 +97,6 @@ class SpinnerErrorButton extends Component {
render() { render() {
const { const {
kind,
isSpinning, isSpinning,
error, error,
children, children,
@@ -113,7 +112,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError; const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK; let iconName = icons.CHECK;
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS; let iconKind = kinds.SUCCESS;
if (hasWarning) { if (hasWarning) {
iconName = icons.WARNING; iconName = icons.WARNING;
@@ -127,7 +126,6 @@ class SpinnerErrorButton extends Component {
return ( return (
<SpinnerButton <SpinnerButton
kind={kind}
isSpinning={isSpinning} isSpinning={isSpinning}
{...otherProps} {...otherProps}
> >
@@ -156,7 +154,6 @@ class SpinnerErrorButton extends Component {
} }
SpinnerErrorButton.propTypes = { SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
children: PropTypes.node.isRequired children: PropTypes.node.isRequired
@@ -10,8 +10,7 @@ class InlineMarkdown extends Component {
render() { render() {
const { const {
className, className,
data, data
blockClassName
} = this.props; } = this.props;
// For now only replace links or code blocks (not both) // For now only replace links or code blocks (not both)
@@ -48,7 +47,7 @@ class InlineMarkdown extends Component {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
} }
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>); markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length; endIndex = match.index + match[0].length;
} }
@@ -67,8 +66,7 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = { InlineMarkdown.propTypes = {
className: PropTypes.string, className: PropTypes.string,
data: PropTypes.string, data: PropTypes.string
blockClassName: PropTypes.string
}; };
export default InlineMarkdown; export default InlineMarkdown;
@@ -32,33 +32,25 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect} onPress={onFilterSelect}
> >
{typeof filter.label === 'function' ? filter.label() : filter.label} {filter.label}
</FilterMenuItem> </FilterMenuItem>
); );
}) })
} }
{ {
customFilters.length > 0 ? customFilters.map((filter) => {
<MenuItemSeparator /> : return (
null <FilterMenuItem
} key={filter.id}
filterKey={filter.id}
{ selectedFilterKey={selectedFilterKey}
customFilters onPress={onFilterSelect}
.sort((a, b) => a.label.localeCompare(b.label)) >
.map((filter) => { {filter.label}
return ( </FilterMenuItem>
<FilterMenuItem );
key={filter.id} })
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
} }
{ {
+4 -4
View File
@@ -7,7 +7,7 @@ function ErrorPage(props) {
const { const {
version, version,
isLocalStorageSupported, isLocalStorageSupported,
translationsError, hasTranslationsError,
authorError, authorError,
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -21,8 +21,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) { if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (translationsError) { } else if (hasTranslationsError) {
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API'); errorMessage = 'Failed to load translations from API';
} else if (authorError) { } else if (authorError) {
errorMessage = getErrorMessage(authorError, 'Failed to load author from API'); errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
} else if (customFiltersError) { } else if (customFiltersError) {
@@ -55,7 +55,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = { ErrorPage.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired,
translationsError: PropTypes.object, hasTranslationsError: PropTypes.bool.isRequired,
authorError: PropTypes.object, authorError: PropTypes.object,
customFiltersError: PropTypes.object, customFiltersError: PropTypes.object,
tagsError: PropTypes.object, tagsError: PropTypes.object,
+10 -20
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchAuthor } from 'Store/Actions/authorActions'; import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions'; import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
@@ -52,7 +52,6 @@ const selectIsPopulated = createSelector(
(state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
( (
customFiltersIsPopulated, customFiltersIsPopulated,
tagsIsPopulated, tagsIsPopulated,
@@ -61,8 +60,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
metadataProfilesIsPopulated, metadataProfilesIsPopulated,
importListsIsPopulated, importListsIsPopulated,
systemStatusIsPopulated, systemStatusIsPopulated
translationsIsPopulated
) => { ) => {
return ( return (
customFiltersIsPopulated && customFiltersIsPopulated &&
@@ -72,8 +70,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
metadataProfilesIsPopulated && metadataProfilesIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
systemStatusIsPopulated && systemStatusIsPopulated
translationsIsPopulated
); );
} }
); );
@@ -87,7 +84,6 @@ const selectErrors = createSelector(
(state) => state.settings.metadataProfiles.error, (state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.system.status.error, (state) => state.system.status.error,
(state) => state.app.translations.error,
( (
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -96,8 +92,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
systemStatusError, systemStatusError
translationsError
) => { ) => {
const hasError = !!( const hasError = !!(
customFiltersError || customFiltersError ||
@@ -107,8 +102,7 @@ const selectErrors = createSelector(
qualityProfilesError || qualityProfilesError ||
metadataProfilesError || metadataProfilesError ||
importListsError || importListsError ||
systemStatusError || systemStatusError
translationsError
); );
return { return {
@@ -120,8 +114,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
systemStatusError, systemStatusError
translationsError
}; };
} }
); );
@@ -183,9 +176,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() { dispatchFetchStatus() {
dispatch(fetchStatus()); dispatch(fetchStatus());
}, },
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) { onResize(dimensions) {
dispatch(saveDimensions(dimensions)); dispatch(saveDimensions(dimensions));
}, },
@@ -220,7 +210,6 @@ class PageConnector extends Component {
this.props.dispatchFetchImportLists(); this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
} }
} }
@@ -236,6 +225,7 @@ class PageConnector extends Component {
render() { render() {
const { const {
hasTranslationsError,
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchAuthor, dispatchFetchAuthor,
@@ -247,15 +237,15 @@ class PageConnector extends Component {
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps ...otherProps
} = this.props; } = this.props;
if (hasError || !this.state.isLocalStorageSupported) { if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
return ( return (
<ErrorPage <ErrorPage
{...this.state} {...this.state}
{...otherProps} {...otherProps}
hasTranslationsError={hasTranslationsError}
/> />
); );
} }
@@ -276,6 +266,7 @@ class PageConnector extends Component {
} }
PageConnector.propTypes = { PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
@@ -289,7 +280,6 @@ PageConnector.propTypes = {
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired onSidebarVisibleChange: PropTypes.func.isRequired
}; };
@@ -21,28 +21,28 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.AUTHOR_CONTINUING, iconName: icons.AUTHOR_CONTINUING,
title: () => translate('Library'), title: 'Library',
to: '/', to: '/',
alias: '/authors', alias: '/authors',
children: [ children: [
{ {
title: () => translate('Authors'), title: 'Authors',
to: '/authors' to: '/authors'
}, },
{ {
title: () => translate('Books'), title: 'Books',
to: '/books' to: '/books'
}, },
{ {
title: () => translate('AddNew'), title: 'Add New',
to: '/add/search' to: '/add/search'
}, },
{ {
title: () => translate('Bookshelf'), title: 'Bookshelf',
to: '/shelf' to: '/shelf'
}, },
{ {
title: () => translate('UnmappedFiles'), title: 'Unmapped Files',
to: '/unmapped' to: '/unmapped'
} }
] ]
@@ -50,26 +50,26 @@ const links = [
{ {
iconName: icons.CALENDAR, iconName: icons.CALENDAR,
title: () => translate('Calendar'), title: 'Calendar',
to: '/calendar' to: '/calendar'
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.ACTIVITY,
title: () => translate('Activity'), title: 'Activity',
to: '/activity/queue', to: '/activity/queue',
children: [ children: [
{ {
title: () => translate('Queue'), title: 'Queue',
to: '/activity/queue', to: '/activity/queue',
statusComponent: QueueStatusConnector statusComponent: QueueStatusConnector
}, },
{ {
title: () => translate('History'), title: 'History',
to: '/activity/history' to: '/activity/history'
}, },
{ {
title: () => translate('Blocklist'), title: 'Blocklist',
to: '/activity/blocklist' to: '/activity/blocklist'
} }
] ]
@@ -77,15 +77,15 @@ const links = [
{ {
iconName: icons.WARNING, iconName: icons.WARNING,
title: () => translate('Wanted'), title: 'Wanted',
to: '/wanted/missing', to: '/wanted/missing',
children: [ children: [
{ {
title: () => translate('Missing'), title: 'Missing',
to: '/wanted/missing' to: '/wanted/missing'
}, },
{ {
title: () => translate('CutoffUnmet'), title: 'Cutoff Unmet',
to: '/wanted/cutoffunmet' to: '/wanted/cutoffunmet'
} }
] ]
@@ -93,55 +93,55 @@ const links = [
{ {
iconName: icons.SETTINGS, iconName: icons.SETTINGS,
title: () => translate('Settings'), title: 'Settings',
to: '/settings', to: '/settings',
children: [ children: [
{ {
title: () => translate('MediaManagement'), title: 'Media Management',
to: '/settings/mediamanagement' to: '/settings/mediamanagement'
}, },
{ {
title: () => translate('Profiles'), title: 'Profiles',
to: '/settings/profiles' to: '/settings/profiles'
}, },
{ {
title: () => translate('Quality'), title: 'Quality',
to: '/settings/quality' to: '/settings/quality'
}, },
{ {
title: () => translate('CustomFormats'), title: translate('CustomFormats'),
to: '/settings/customformats' to: '/settings/customformats'
}, },
{ {
title: () => translate('Indexers'), title: translate('Indexers'),
to: '/settings/indexers' to: '/settings/indexers'
}, },
{ {
title: () => translate('DownloadClients'), title: 'Download Clients',
to: '/settings/downloadclients' to: '/settings/downloadclients'
}, },
{ {
title: () => translate('ImportLists'), title: 'Import Lists',
to: '/settings/importlists' to: '/settings/importlists'
}, },
{ {
title: () => translate('Connect'), title: 'Connect',
to: '/settings/connect' to: '/settings/connect'
}, },
{ {
title: () => translate('Metadata'), title: 'Metadata',
to: '/settings/metadata' to: '/settings/metadata'
}, },
{ {
title: () => translate('Tags'), title: 'Tags',
to: '/settings/tags' to: '/settings/tags'
}, },
{ {
title: () => translate('General'), title: 'General',
to: '/settings/general' to: '/settings/general'
}, },
{ {
title: () => translate('Ui'), title: 'UI',
to: '/settings/ui' to: '/settings/ui'
} }
] ]
@@ -149,32 +149,32 @@ const links = [
{ {
iconName: icons.SYSTEM, iconName: icons.SYSTEM,
title: () => translate('System'), title: 'System',
to: '/system/status', to: '/system/status',
children: [ children: [
{ {
title: () => translate('Status'), title: 'Status',
to: '/system/status', to: '/system/status',
statusComponent: HealthStatusConnector statusComponent: HealthStatusConnector
}, },
{ {
title: () => translate('Tasks'), title: 'Tasks',
to: '/system/tasks' to: '/system/tasks'
}, },
{ {
title: () => translate('Backup'), title: 'Backup',
to: '/system/backup' to: '/system/backup'
}, },
{ {
title: () => translate('Updates'), title: 'Updates',
to: '/system/updates' to: '/system/updates'
}, },
{ {
title: () => translate('Events'), title: 'Events',
to: '/system/events' to: '/system/events'
}, },
{ {
title: () => translate('LogFiles'), title: 'Log Files',
to: '/system/logs/files' to: '/system/logs/files'
} }
] ]
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
} }
<span className={isChildItem ? styles.noIcon : null}> <span className={isChildItem ? styles.noIcon : null}>
{typeof title === 'function' ? title() : title} {title}
</span> </span>
{ {
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = { PageSidebarItem.propTypes = {
iconName: PropTypes.object, iconName: PropTypes.object,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, title: PropTypes.string.isRequired,
to: PropTypes.string.isRequired, to: PropTypes.string.isRequired,
isActive: PropTypes.bool, isActive: PropTypes.bool,
isActiveParent: PropTypes.bool, isActiveParent: PropTypes.bool,
@@ -202,8 +202,6 @@ class SignalRConnector extends Component {
this.props.dispatchUpdateItem({ section, ...body.resource }); this.props.dispatchUpdateItem({ section, ...body.resource });
} else if (body.action === 'deleted') { } else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id }); this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('bookFileDeleted');
} }
// Repopulate the page to handle recently imported file // Repopulate the page to handle recently imported file
+1 -3
View File
@@ -1,10 +1,8 @@
import React from 'react'; import React from 'react';
type PropertyFunction<T> = () => T;
interface Column { interface Column {
name: string; name: string;
label: string | PropertyFunction<string> | React.ReactNode; label: string | React.ReactNode;
columnLabel?: string; columnLabel?: string;
isSortable?: boolean; isSortable?: boolean;
isVisible: boolean; isVisible: boolean;
+1 -1
View File
@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)} {...getTableHeaderCellProps(otherProps)}
{...column} {...column}
> >
{typeof column.label === 'function' ? column.label() : column.label} {column.label}
</TableHeaderCell> </TableHeaderCell>
); );
}) })
@@ -30,7 +30,6 @@ class TableHeaderCell extends Component {
const { const {
className, className,
name, name,
label,
columnLabel, columnLabel,
isSortable, isSortable,
isVisible, isVisible,
@@ -54,8 +53,7 @@ class TableHeaderCell extends Component {
{...otherProps} {...otherProps}
component="th" component="th"
className={className} className={className}
label={typeof label === 'function' ? label() : label} title={columnLabel}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress} onPress={this.onPress}
> >
{children} {children}
@@ -79,8 +77,7 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = { TableHeaderCell.propTypes = {
className: PropTypes.string, className: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]), columnLabel: PropTypes.string,
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool, isSortable: PropTypes.bool,
isVisible: PropTypes.bool, isVisible: PropTypes.bool,
isModifiable: PropTypes.bool, isModifiable: PropTypes.bool,
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false} isDisabled={isModifiable === false}
onChange={onVisibleChange} onChange={onVisibleChange}
/> />
{typeof label === 'function' ? label() : label} {label}
</label> </label>
{ {
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = { TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn <TableOptionsColumn
name={name} name={name}
label={typeof label === 'function' ? label() : label} label={label}
isVisible={isVisible} isVisible={isVisible}
isModifiable={isModifiable} isModifiable={isModifiable}
index={index} index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = { TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
@@ -15,5 +15,5 @@
"start_url": "../../../../", "start_url": "../../../../",
"theme_color": "#3a3f51", "theme_color": "#3a3f51",
"background_color": "#3a3f51", "background_color": "#3a3f51",
"display": "minimal-ui" "display": "standalone"
} }
-120
View File
@@ -1,120 +0,0 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
// This file contains some helpers for power users in a browser console
let hasWarned = false;
function checkActivationWarning() {
if (!hasWarned) {
console.log('Activated ReadarrApi console helpers.');
console.warn('Be warned: There will be no further confirmation checks.');
hasWarned = true;
}
}
function attachAsyncActions(promise) {
promise.filter = function() {
const args = arguments;
const res = this.then((d) => d.filter(...args));
attachAsyncActions(res);
return res;
};
promise.map = function() {
const args = arguments;
const res = this.then((d) => d.map(...args));
attachAsyncActions(res);
return res;
};
promise.all = function() {
const res = this.then((d) => Promise.all(d));
attachAsyncActions(res);
return res;
};
promise.forEach = function(action) {
const res = this.then((d) => Promise.all(d.map(action)));
attachAsyncActions(res);
return res;
};
}
class ResourceApi {
constructor(api, url) {
this.api = api;
this.url = url;
}
single(id) {
return this.api.fetch(`${this.url}/${id}`);
}
all() {
return this.api.fetch(this.url);
}
filter(pred) {
return this.all().filter(pred);
}
update(resource) {
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
}
delete(resource) {
if (typeof resource === 'object' && resource !== null && resource.id) {
resource = resource.id;
}
if (!resource || !Number.isInteger(resource)) {
throw Error('Invalid resource', resource);
}
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
}
fetch(url, options) {
return this.api.fetch(`${this.url}${url}`, options);
}
}
class ConsoleApi {
constructor() {
this.author = new ResourceApi(this, '/author');
}
resource(url) {
return new ResourceApi(this, url);
}
fetch(url, options) {
checkActivationWarning();
options = options || {};
const req = {
url,
method: options.method || 'GET'
};
if (options.data) {
req.dataType = 'json';
req.data = JSON.stringify(options.data);
}
const promise = createAjaxRequest(req).request;
promise.fail((xhr) => {
console.error(`Failed to fetch ${url}`, xhr);
});
attachAsyncActions(promise);
return promise;
}
}
window.ReadarrApi = new ConsoleApi();
export default ConsoleApi;
-2
View File
@@ -6,7 +6,6 @@ export const BOOKSHELF = 'bookshelf';
export const KEY_VALUE_LIST = 'keyValueList'; export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect'; export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
@@ -35,7 +34,6 @@ export const all = [
KEY_VALUE_LIST, KEY_VALUE_LIST,
MONITOR_BOOKS_SELECT, MONITOR_BOOKS_SELECT,
MONITOR_NEW_ITEMS_SELECT, MONITOR_NEW_ITEMS_SELECT,
FLOAT,
NUMBER, NUMBER,
OAUTH, OAUTH,
PASSWORD, PASSWORD,
@@ -69,7 +69,7 @@ const columns = [
name: 'customFormats', name: 'customFormats',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.INTERACTIVE, name: icons.INTERACTIVE,
title: () => translate('CustomFormat') title: translate('CustomFormat')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -91,9 +91,9 @@ const filterExistingFilesOptions = {
}; };
const importModeOptions = [ const importModeOptions = [
{ key: 'chooseImportMode', value: () => translate('ChooseImportMethod'), disabled: true }, { key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: () => translate('MoveFiles') }, { key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: () => translate('HardlinkCopyFiles') } { key: 'copy', value: translate('HardlinkCopyFiles') }
]; ];
const SELECT = 'select'; const SELECT = 'select';
@@ -1,11 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sortDirections } from 'Helpers/Props'; import { icons, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchRow from './InteractiveSearchRow'; import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css'; import styles from './InteractiveSearch.css';
@@ -57,7 +56,7 @@ const columns = [
name: 'customFormatScore', name: 'customFormatScore',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.SCORE, name: icons.SCORE,
title: () => translate('CustomFormatScore') title: translate('CustomFormatScore')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -113,17 +112,17 @@ function InteractiveSearch(props) {
{ {
!isFetching && isPopulated && !totalReleasesCount ? !isFetching && isPopulated && !totalReleasesCount ?
<Alert kind={kinds.INFO}> <div className={styles.blankpad}>
{translate('NoResultsFound')} No results found
</Alert> : </div> :
null null
} }
{ {
!!totalReleasesCount && isPopulated && !items.length ? !!totalReleasesCount && isPopulated && !items.length ?
<Alert kind={kinds.WARNING}> <div className={styles.blankpad}>
{translate('AllResultsAreHiddenByTheAppliedFilter')} All results are hidden by the applied filter
</Alert> : </div> :
null null
} }
@@ -158,7 +157,7 @@ function InteractiveSearch(props) {
{ {
totalReleasesCount !== items.length && !!items.length ? totalReleasesCount !== items.length && !!items.length ?
<div className={styles.filteredMessage}> <div className={styles.filteredMessage}>
{translate('SomeResultsAreHiddenByTheAppliedFilter')} Some results are hidden by the applied filter
</div> : </div> :
null null
} }
@@ -28,10 +28,6 @@
text-align: center; text-align: center;
} }
.quality {
white-space: nowrap;
}
.customFormatScore { .customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
@@ -32,18 +32,6 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD; return icons.DOWNLOAD;
} }
function getDownloadKind(isGrabbed, grabError, downloadAllowed) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError || !downloadAllowed) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) { if (isGrabbing) {
return ''; return '';
@@ -178,7 +166,7 @@ class InteractiveSearchRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.quality}> <TableRowCell className={styles.quality}>
<BookQuality quality={quality} showRevision={true} /> <BookQuality quality={quality} />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
@@ -224,7 +212,7 @@ class InteractiveSearchRow extends Component {
{ {
<SpinnerIconButton <SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)} name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError, downloadAllowed)} kind={grabError || !downloadAllowed ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)} title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing} isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress} onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen} isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCustomFormat')} title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { name })} message={translate('DeleteCustomFormatMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat} onConfirm={this.onConfirmDeleteCustomFormat}
@@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) {
{...otherProps} {...otherProps}
> >
{ {
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) && fields && fields.some((x) => x.label === 'Regular Expression') &&
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
<div> <div>
<div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} /> <div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
@@ -115,7 +115,7 @@ class Specification extends Component {
isOpen={this.state.isDeleteSpecificationModalOpen} isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCondition')} title={translate('DeleteCondition')}
message={translate('DeleteConditionMessageText', { name })} message={translate('DeleteConditionMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification} onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose} onCancel={this.onDeleteSpecificationModalClose}
@@ -113,7 +113,7 @@ class DownloadClient extends Component {
isOpen={this.state.isDeleteDownloadClientModalOpen} isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteDownloadClient')} title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', { name })} message={translate('DeleteDownloadClientMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient} onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose} onCancel={this.onDeleteDownloadClientModalClose}
@@ -27,25 +27,9 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const enableOptions = [
{ { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
key: NO_CHANGE, { key: 'enabled', value: translate('Enabled') },
get value() { { key: 'disabled', value: translate('Disabled') },
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
]; ];
function ManageDownloadClientsEditModalContent( function ManageDownloadClientsEditModalContent(
@@ -180,7 +164,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountDownloadClientsSelected', { selectedCount })} {translate('CountDownloadClientsSelected', [selectedCount])}
</div> </div>
<div> <div>
@@ -14,11 +14,9 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteDownloadClients, bulkDeleteDownloadClients,
bulkEditDownloadClients, bulkEditDownloadClients,
setManageDownloadClientsSort,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
@@ -38,37 +36,37 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: () => translate('Implementation'), label: translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enable', name: 'enable',
label: () => translate('Enabled'), label: translate('Enabled'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'priority', name: 'priority',
label: () => translate('Priority'), label: translate('Priority'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeCompletedDownloads', name: 'removeCompletedDownloads',
label: () => translate('RemoveCompleted'), label: translate('RemoveCompleted'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeFailedDownloads', name: 'removeFailedDownloads',
label: () => translate('RemoveFailed'), label: translate('RemoveFailed'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -82,8 +80,6 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps { interface ManageDownloadClientsModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageDownloadClientsModalContent( function ManageDownloadClientsModalContent(
@@ -98,8 +94,6 @@ function ManageDownloadClientsModalContent(
isSaving, isSaving,
error, error,
items, items,
sortKey,
sortDirection,
}: DownloadClientAppState = useSelector( }: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients') createClientSideCollectionSelector('settings.downloadClients')
); );
@@ -120,13 +114,6 @@ function ManageDownloadClientsModalContent(
const selectedCount = selectedIds.length; const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageDownloadClientsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => { const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]); }, [setIsDeleteModalOpen]);
@@ -232,9 +219,6 @@ function ManageDownloadClientsModalContent(
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{items.map((item) => { {items.map((item) => {
@@ -302,9 +286,9 @@ function ManageDownloadClientsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedDownloadClients')} title={translate('DeleteSelectedDownloadClients')}
message={translate('DeleteSelectedDownloadClientsMessageText', { message={translate('DeleteSelectedDownloadClientsMessageText', [
count: selectedIds.length, selectedIds.length,
})} ])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -72,24 +72,9 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ { key: 'add', value: translate('Add') },
key: 'add', { key: 'remove', value: translate('Remove') },
get value() { { key: 'replace', value: translate('Replace') },
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
]; ];
return ( return (
@@ -61,12 +61,8 @@ function DownloadClientOptions(props) {
legend={translate('FailedDownloadHandling')} legend={translate('FailedDownloadHandling')}
> >
<Form> <Form>
<FormGroup <FormGroup size={sizes.MEDIUM}>
advancedSettings={advancedSettings} <FormLabel>{translate('RedownloadFailed')}</FormLabel>
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
@@ -76,28 +72,7 @@ function DownloadClientOptions(props) {
{...settings.autoRedownloadFailed} {...settings.autoRedownloadFailed}
/> />
</FormGroup> </FormGroup>
{
settings.autoRedownloadFailed.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="autoRedownloadFailedFromInteractiveSearch"
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
onChange={onInputChange}
{...settings.autoRedownloadFailedFromInteractiveSearch}
/>
</FormGroup> :
null
}
</Form> </Form>
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
{translate('RemoveDownloadsAlert')} {translate('RemoveDownloadsAlert')}
</Alert> </Alert>

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