Compare commits

..

1 Commits

Author SHA1 Message Date
Michon van Dooren 6a556a89aa Fixed: Include preferred size in quality definition reset
(cherry picked from commit 8e925ac76d2f46cf5fef1ea62a20ae5e85d3000e)
2023-07-30 16:46:46 +00:00
430 changed files with 3465 additions and 8125 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.
+2 -1
View File
@@ -15,7 +15,8 @@ jobs:
issue-comment: > issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively :wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears 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). 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 close-issue: true
lock-issue: false lock-issue: false
- uses: dessant/support-requests@v3 - uses: dessant/support-requests@v3
+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.14' majorVersion: '0.3.1'
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)
+10 -9
View File
@@ -23,7 +23,7 @@ UpdateVersionNumber()
echo "Updating Version Info" echo "Updating Version Info"
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props
sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" distribution/osx/Readarr.app/Contents/Info.plist sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" macOS/Readarr.app/Contents/Info.plist
fi fi
} }
@@ -183,7 +183,7 @@ PackageMacOSApp()
rm -rf $folder rm -rf $folder
mkdir -p $folder mkdir -p $folder
cp -r distribution/osx/Readarr.app $folder cp -r macOS/Readarr.app $folder
mkdir -p $folder/Readarr.app/Contents/MacOS mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries" echo "Copying Binaries"
@@ -245,7 +245,7 @@ BuildInstaller()
local framework="$1" local framework="$1"
local runtime="$2" local runtime="$2"
./_inno/ISCC.exe distribution/windows/setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime" ./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
} }
InstallInno() InstallInno()
@@ -391,21 +391,22 @@ then
fi fi
fi fi
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]]; if [ "$FRONTEND" = "YES" ];
then then
YarnInstall YarnInstall
RunWebpack
fi fi
if [ "$LINT" = "YES" ]; if [ "$LINT" = "YES" ];
then then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI LintUI
fi fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ]; if [ "$PACKAGES" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
+4 -4
View File
@@ -4,14 +4,14 @@ module.exports = {
plugins: [ plugins: [
// 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({
-4
View File
@@ -338,8 +338,4 @@ Queue.propTypes = {
onRemoveSelectedPress: PropTypes.func.isRequired onRemoveSelectedPress: PropTypes.func.isRequired
}; };
Queue.defaultProps = {
count: 0
};
export default Queue; export default Queue;
+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>
{ {
@@ -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,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;
@@ -7,7 +7,6 @@ import TableBody from 'Components/Table/TableBody';
import { 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 = [
{ {
@@ -65,7 +64,7 @@ class AuthorHistoryTableContent extends Component {
const hasItems = !!items.length; const hasItems = !!items.length;
return ( return (
<div> <>
{ {
isFetching && isFetching &&
<LoadingIndicator /> <LoadingIndicator />
@@ -80,7 +79,7 @@ class AuthorHistoryTableContent extends Component {
{ {
isPopulated && !hasItems && !error && isPopulated && !hasItems && !error &&
<div className={styles.blankpad}> <div>
{translate('NoHistory')} {translate('NoHistory')}
</div> </div>
} }
@@ -104,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 {
@@ -6,5 +6,4 @@
.statusIcon { .statusIcon {
width: 20px !important; width: 20px !important;
text-align: center;
} }
+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,
@@ -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,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 BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent'; import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent';
function BookInteractiveSearchModal(props) { function BookInteractiveSearchModal(props) {
@@ -15,7 +14,6 @@ function BookInteractiveSearchModal(props) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false} closeOnBackgroundClick={false}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
@@ -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));
@@ -9,10 +9,6 @@
&:hover { &:hover {
background-color: var(--inputHoverBackgroundColor); background-color: var(--inputHoverBackgroundColor);
} }
&.isDisabled {
cursor: not-allowed;
}
} }
.optionCheck { .optionCheck {
+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,7 +32,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect} onPress={onFilterSelect}
> >
{typeof filter.label === 'function' ? filter.label() : filter.label} {filter.label}
</FilterMenuItem> </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,
+1 -7
View File
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { tooltipPositions } from 'Helpers/Props';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
import styles from './Popover.css'; import styles from './Popover.css';
@@ -31,13 +30,8 @@ function Popover(props) {
} }
Popover.propTypes = { Popover.propTypes = {
className: PropTypes.string,
bodyClassName: PropTypes.string,
anchor: PropTypes.node.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
position: PropTypes.oneOf(tooltipPositions.all),
canFlip: PropTypes.bool
}; };
export default Popover; export default Popover;
@@ -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;
@@ -1,8 +0,0 @@
enum TooltipPosition {
Top = 'top',
Right = 'right',
Bottom = 'bottom',
Left = 'left',
}
export default TooltipPosition;
-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
} }
@@ -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 '';
@@ -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}
@@ -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>
@@ -36,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,
}, },
@@ -286,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 (
@@ -1,12 +1,10 @@
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 FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageSectionContent from 'Components/Page/PageSectionContent'; import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons, kinds } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
import RemotePathMapping from './RemotePathMapping'; import RemotePathMapping from './RemotePathMapping';
@@ -52,11 +50,6 @@ class RemotePathMappings extends Component {
errorMessage={translate('UnableToLoadRemotePathMappings')} errorMessage={translate('UnableToLoadRemotePathMappings')}
{...otherProps} {...otherProps}
> >
<Alert kind={kinds.INFO}>
<InlineMarkdown data={translate('RemotePathMappingsInfo', { app: 'Readarr', wikiLink: 'https://wiki.servarr.com/readarr/settings#remote-path-mappings' })} />
</Alert>
<div className={styles.remotePathMappingsHeader}> <div className={styles.remotePathMappingsHeader}>
<div className={styles.host}> <div className={styles.host}>
{translate('Host')} {translate('Host')}
@@ -103,6 +103,7 @@ class GeneralSettings extends Component {
isResettingApiKey, isResettingApiKey,
isWindows, isWindows,
isWindowsService, isWindowsService,
isDocker,
mode, mode,
packageUpdateMechanism, packageUpdateMechanism,
onInputChange, onInputChange,
@@ -170,6 +171,7 @@ class GeneralSettings extends Component {
settings={settings} settings={settings}
isWindows={isWindows} isWindows={isWindows}
packageUpdateMechanism={packageUpdateMechanism} packageUpdateMechanism={packageUpdateMechanism}
isDocker={isDocker}
onInputChange={onInputChange} onInputChange={onInputChange}
/> />
@@ -212,6 +214,7 @@ GeneralSettings.propTypes = {
hasSettings: PropTypes.bool.isRequired, hasSettings: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired,
isWindowsService: PropTypes.bool.isRequired, isWindowsService: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired, packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
@@ -26,6 +26,7 @@ function createMapStateToProps() {
isResettingApiKey, isResettingApiKey,
isWindows: systemStatus.isWindows, isWindows: systemStatus.isWindows,
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service', isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
isDocker: systemStatus.isDocker,
mode: systemStatus.mode, mode: systemStatus.mode,
packageUpdateMechanism: systemStatus.packageUpdateMechanism, packageUpdateMechanism: systemStatus.packageUpdateMechanism,
...sectionSettings ...sectionSettings
+25 -13
View File
@@ -8,17 +8,12 @@ import { inputTypes, sizes } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const branchValues = [
'master',
'develop',
'nightly'
];
function UpdateSettings(props) { function UpdateSettings(props) {
const { const {
advancedSettings, advancedSettings,
settings, settings,
isWindows, isWindows,
isDocker,
packageUpdateMechanism, packageUpdateMechanism,
onInputChange onInputChange
} = props; } = props;
@@ -49,21 +44,32 @@ function UpdateSettings(props) {
updateOptions.push({ key: 'script', value: 'Script' }); updateOptions.push({ key: 'script', value: 'Script' });
if (isDocker) {
return (
<FieldSet legend={translate('Updates')}>
<div>
{translate('UpdatingIsDisabledInsideADockerContainerUpdateTheContainerImageInstead')}
</div>
</FieldSet>
);
}
return ( return (
<FieldSet legend={translate('Updates')}> <FieldSet legend={translate('Updates')}>
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>{translate('Branch')}</FormLabel> <FormLabel>
{translate('Branch')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.AUTO_COMPLETE} type={inputTypes.TEXT}
name="branch" name="branch"
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')} helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr" helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
{...branch} {...branch}
values={branchValues}
onChange={onInputChange} onChange={onInputChange}
readOnly={usingExternalUpdateMechanism} readOnly={usingExternalUpdateMechanism}
/> />
@@ -77,13 +83,14 @@ function UpdateSettings(props) {
isAdvanced={true} isAdvanced={true}
size={sizes.MEDIUM} size={sizes.MEDIUM}
> >
<FormLabel>{translate('Automatic')}</FormLabel> <FormLabel>
{translate('Automatic')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="updateAutomatically" name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')} helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
onChange={onInputChange} onChange={onInputChange}
{...updateAutomatically} {...updateAutomatically}
/> />
@@ -93,7 +100,9 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>{translate('Mechanism')}</FormLabel> <FormLabel>
{translate('Mechanism')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
@@ -112,7 +121,9 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>{translate('ScriptPath')}</FormLabel> <FormLabel>
{translate('ScriptPath')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
@@ -133,6 +144,7 @@ UpdateSettings.propTypes = {
advancedSettings: PropTypes.bool.isRequired, advancedSettings: PropTypes.bool.isRequired,
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
isWindows: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired, packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired onInputChange: PropTypes.func.isRequired
}; };
@@ -8,13 +8,11 @@
} }
.name { .name {
@add-mixin truncate; flex: 1 0 300px;
flex: 0 1 600px;
} }
.foreignId { .foreignId {
flex: 0 0 100px; flex: 0 0 200px;
} }
.actions { .actions {
@@ -4,12 +4,12 @@
font-weight: bold; font-weight: bold;
} }
.name { .foreignId {
flex: 0 1 600px; flex: 0 0 200px;
} }
.foreignId { .name {
flex: 0 0 100px; flex: 1 0 300px;
} }
.addImportListExclusion { .addImportListExclusion {
@@ -107,7 +107,7 @@ class ImportList extends Component {
isOpen={this.state.isDeleteImportListModalOpen} isOpen={this.state.isDeleteImportListModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteImportList')} title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', { name })} message={translate('DeleteImportListMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportList} onConfirm={this.onConfirmDeleteImportList}
onCancel={this.onDeleteImportListModalClose} onCancel={this.onDeleteImportListModalClose}
@@ -27,25 +27,9 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const autoAddOptions = [ const autoAddOptions = [
{ { 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 ManageImportListsEditModalContent( function ManageImportListsEditModalContent(
@@ -184,7 +168,7 @@ function ManageImportListsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountImportListsSelected', { selectedCount })} {translate('CountImportListsSelected', [selectedCount])}
</div> </div>
<div> <div>
@@ -36,43 +36,43 @@ 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: 'qualityProfileId', name: 'qualityProfileId',
label: () => translate('QualityProfile'), label: translate('QualityProfile'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'metadataProfileId', name: 'metadataProfileId',
label: () => translate('MetadataProfile'), label: translate('MetadataProfile'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'rootFolderPath', name: 'rootFolderPath',
label: () => translate('RootFolder'), label: translate('RootFolder'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableAutomaticAdd', name: 'enableAutomaticAdd',
label: () => translate('AutoAdd'), label: translate('AutoAdd'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'tags', name: 'tags',
label: () => translate('Tags'), label: translate('Tags'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -283,9 +283,9 @@ function ManageImportListsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedImportLists')} title={translate('DeleteSelectedImportLists')}
message={translate('DeleteSelectedImportListsMessageText', { message={translate('DeleteSelectedImportListsMessageText', [
count: selectedIds.length, selectedIds.length,
})} ])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -70,24 +70,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 (
@@ -152,7 +152,7 @@ class Indexer extends Component {
isOpen={this.state.isDeleteIndexerModalOpen} isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteIndexer')} title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })} message={translate('DeleteIndexerMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer} onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose} onCancel={this.onDeleteIndexerModalClose}
@@ -27,25 +27,9 @@ interface ManageIndexersEditModalContentProps {
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 ManageIndexersEditModalContent( function ManageIndexersEditModalContent(
@@ -178,7 +162,7 @@ function ManageIndexersEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', { selectedCount })} {translate('CountIndexersSelected', [selectedCount])}
</div> </div>
<div> <div>
@@ -36,43 +36,43 @@ 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: 'enableRss', name: 'enableRss',
label: () => translate('EnableRSS'), label: translate('EnableRSS'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableAutomaticSearch', name: 'enableAutomaticSearch',
label: () => translate('EnableAutomaticSearch'), label: translate('EnableAutomaticSearch'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableInteractiveSearch', name: 'enableInteractiveSearch',
label: () => translate('EnableInteractiveSearch'), label: translate('EnableInteractiveSearch'),
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: 'tags', name: 'tags',
label: () => translate('Tags'), label: translate('Tags'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedIndexers')} title={translate('DeleteSelectedIndexers')}
message={translate('DeleteSelectedIndexersMessageText', { message={translate('DeleteSelectedIndexersMessageText', [
count: selectedIds.length, selectedIds.length,
})} ])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -70,24 +70,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 (
@@ -212,24 +212,26 @@ class MediaManagement extends Component {
</FormGroup> </FormGroup>
{ {
settings.importExtraFiles.value ? settings.importExtraFiles.value &&
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>{translate('ImportExtraFiles')}</FormLabel> <FormLabel>
{translate('ImportExtraFiles')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="extraFileExtensions" name="extraFileExtensions"
helpTexts={[ helpTexts={[
translate('ExtraFileExtensionsHelpText'), translate('ExtraFileExtensionsHelpTexts1'),
translate('ExtraFileExtensionsHelpTextsExamples') translate('ExtraFileExtensionsHelpTexts2')
]} ]}
onChange={onInputChange} onChange={onInputChange}
{...settings.extraFileExtensions} {...settings.extraFileExtensions}
/> />
</FormGroup> : null </FormGroup>
} }
</FieldSet> </FieldSet>
} }
@@ -95,7 +95,7 @@ class RootFolder extends Component {
isOpen={this.state.isDeleteRootFolderModalOpen} isOpen={this.state.isDeleteRootFolderModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteRootFolder')} title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', { name })} message={translate('DeleteRootFolderMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRootFolder} onConfirm={this.onConfirmDeleteRootFolder}
onCancel={this.onDeleteRootFolderModalClose} onCancel={this.onDeleteRootFolderModalClose}
@@ -11,51 +11,16 @@ import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const writeAudioTagOptions = [ const writeAudioTagOptions = [
{ { key: 'no', value: translate('WriteTagsNo') },
key: 'no', { key: 'sync', value: translate('WriteTagsSync') },
get value() { { key: 'allFiles', value: translate('WriteTagsAll') },
return translate('WriteTagsNo'); { key: 'newFiles', value: translate('WriteTagsNew') }
}
},
{
key: 'sync',
get value() {
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
]; ];
const writeBookTagOptions = [ const writeBookTagOptions = [
{ { key: 'sync', value: translate('WriteTagsSync') },
key: 'sync', { key: 'allFiles', value: translate('WriteTagsAll') },
get value() { { key: 'newFiles', value: translate('WriteTagsNew') }
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
]; ];
function MetadataProvider(props) { function MetadataProvider(props) {
@@ -60,7 +60,6 @@ class Notification extends Component {
onReleaseImport, onReleaseImport,
onUpgrade, onUpgrade,
onRename, onRename,
onAuthorAdded,
onAuthorDelete, onAuthorDelete,
onBookDelete, onBookDelete,
onBookFileDelete, onBookFileDelete,
@@ -74,7 +73,6 @@ class Notification extends Component {
supportsOnReleaseImport, supportsOnReleaseImport,
supportsOnUpgrade, supportsOnUpgrade,
supportsOnRename, supportsOnRename,
supportsOnAuthorAdded,
supportsOnAuthorDelete, supportsOnAuthorDelete,
supportsOnBookDelete, supportsOnBookDelete,
supportsOnBookFileDelete, supportsOnBookFileDelete,
@@ -138,14 +136,6 @@ class Notification extends Component {
null null
} }
{
supportsOnAuthorAdded && onAuthorAdded ?
<Label kind={kinds.SUCCESS}>
{translate('OnAuthorAdded')}
</Label> :
null
}
{ {
supportsOnAuthorDelete && onAuthorDelete ? supportsOnAuthorDelete && onAuthorDelete ?
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
@@ -237,7 +227,7 @@ class Notification extends Component {
isOpen={this.state.isDeleteNotificationModalOpen} isOpen={this.state.isDeleteNotificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteNotification')} title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', { name })} message={translate('DeleteNotificationMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteNotification} onConfirm={this.onConfirmDeleteNotification}
onCancel={this.onDeleteNotificationModalClose} onCancel={this.onDeleteNotificationModalClose}
@@ -254,7 +244,6 @@ Notification.propTypes = {
onReleaseImport: PropTypes.bool.isRequired, onReleaseImport: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired, onUpgrade: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired, onRename: PropTypes.bool.isRequired,
onAuthorAdded: PropTypes.bool.isRequired,
onAuthorDelete: PropTypes.bool.isRequired, onAuthorDelete: PropTypes.bool.isRequired,
onBookDelete: PropTypes.bool.isRequired, onBookDelete: PropTypes.bool.isRequired,
onBookFileDelete: PropTypes.bool.isRequired, onBookFileDelete: PropTypes.bool.isRequired,
@@ -268,7 +257,6 @@ Notification.propTypes = {
supportsOnReleaseImport: PropTypes.bool.isRequired, supportsOnReleaseImport: PropTypes.bool.isRequired,
supportsOnUpgrade: PropTypes.bool.isRequired, supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired, supportsOnRename: PropTypes.bool.isRequired,
supportsOnAuthorAdded: PropTypes.bool.isRequired,
supportsOnAuthorDelete: PropTypes.bool.isRequired, supportsOnAuthorDelete: PropTypes.bool.isRequired,
supportsOnBookDelete: PropTypes.bool.isRequired, supportsOnBookDelete: PropTypes.bool.isRequired,
supportsOnBookFileDelete: PropTypes.bool.isRequired, supportsOnBookFileDelete: PropTypes.bool.isRequired,

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