mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b34e0f8259 | |||
| 4c170d0452 | |||
| 6dc0a88004 | |||
| 33b44a8a53 | |||
| cb72e752f9 | |||
| a11ee7bc11 | |||
| 98d60e1a8e | |||
| 6377c688fc | |||
| 7a37f130f9 | |||
| 724dd7e733 | |||
| e1be3b20e9 | |||
| 2f041f9ec1 | |||
| f10ccf587d | |||
| 0242b40eda | |||
| 7a768b5d0f | |||
| a57254640f | |||
| 1a6f45bafd | |||
| c6071f6d81 | |||
| 2a47a237d4 | |||
| a7607ac7d6 | |||
| 43797b326d | |||
| 5c4f829993 | |||
| 8dd8c95f36 | |||
| 6f6036a199 | |||
| 625e500132 | |||
| 39575b1248 | |||
| f1d343218c | |||
| b0829d5537 | |||
| 965e7c22d9 | |||
| 75535e61d9 | |||
| c0b17d9345 | |||
| 84e657482d | |||
| ed27bcf213 | |||
| 9ee2fe6f5c | |||
| d5e19b8c3c | |||
| 2957b40512 | |||
| 9f46fc923d | |||
| 7dc1e47504 | |||
| d15c116f13 | |||
| dd704579df | |||
| bd9d4b484c | |||
| 913b845faa | |||
| 6e81517d51 | |||
| 34e74eecd7 | |||
| 895eccebc5 | |||
| f722d49b3a | |||
| cac97c057f | |||
| 63e132d257 | |||
| 6ab1d8e16b | |||
| 80630bf97f | |||
| 904285045b | |||
| 1006ec6b52 | |||
| 4cb1100704 | |||
| 745b92daf4 | |||
| 9eafdbd1af | |||
| 200396ef7a | |||
| c5a724f14e | |||
| 42b11528b4 | |||
| e2210228b3 | |||
| ded7c3c6e2 | |||
| e1c6722aad | |||
| e17655c26a | |||
| e66c628241 | |||
| 8f0514a91d | |||
| d7aea82e45 | |||
| 19db75b36b | |||
| 11a18b534a | |||
| 70807a9dcf | |||
| 350600607d | |||
| e9f0c96249 | |||
| d9acbf5682 | |||
| 07cbd7c8d2 | |||
| 0ea189d03c | |||
| 9e3f9f9618 | |||
| 68c326ae27 | |||
| 46367d2023 | |||
| b64c52a846 | |||
| 345854d0fe | |||
| 31baed4b2c | |||
| 7d0d503a5e | |||
| 9f50166fa6 | |||
| 3c1ca6ea4e | |||
| 3cd4c67ba1 | |||
| fc3a2e9ab2 | |||
| a71d40edba | |||
| 9ba5850fca | |||
| 0d06418194 | |||
| f95dd00b51 | |||
| 271266b10a | |||
| cab93249ec | |||
| 8921c5d7a0 | |||
| dbbf1a7f58 | |||
| 69f99373e5 | |||
| 7be5732a3a | |||
| e66ba84fc0 | |||
| c0b30a5028 | |||
| 3cf4d2907e | |||
| ae96ebca57 | |||
| d336aaf3f0 | |||
| ec40bc6eea | |||
| 75bb34afaa | |||
| de1cc25c90 | |||
| 9884f6f282 | |||
| e792db4d33 | |||
| 2dbf5b5a71 | |||
| c6bb6ad878 | |||
| bfd24da2d9 | |||
| 8dd3b45c90 | |||
| 0b0f21d0ac | |||
| 738f5c58ad | |||
| 9a7b5bf14e | |||
| e1260d504e | |||
| 736651324f | |||
| 489f03441b | |||
| e4b5d559df | |||
| 07fbb0d1f4 | |||
| 666455f9b1 | |||
| 091449d9bf | |||
| f87a66fcba | |||
| 1bba7e177b | |||
| 57445bbe57 | |||
| ec91142c85 | |||
| 0685896ed8 | |||
| ee0048c768 | |||
| 16e5ffa467 | |||
| 431c66c7c1 | |||
| 57bd6539c8 | |||
| 637cb1711d | |||
| 7e011df2b2 | |||
| 79907c881c | |||
| db6a627983 | |||
| d76a489be6 | |||
| fd17df0dd0 | |||
| 53cf530893 | |||
| 91f33c670e | |||
| d322619733 | |||
| 1182798929 | |||
| ad0249c7db | |||
| 3259e6dc10 | |||
| 541d3307e1 | |||
| ad60352bae | |||
| 02e051580c | |||
| b8964f8bba | |||
| 53f6f64448 | |||
| 9044ecc59b | |||
| eff0408b0e | |||
| fef525ddb8 | |||
| 3454f1c9ed | |||
| 21666df8f1 | |||
| fd58e9671c | |||
| 8971ac2e14 | |||
| a99fc6fa66 | |||
| 587b600d6c | |||
| 06b86d4fad | |||
| 6b92b556bb | |||
| a49cf72869 | |||
| 070919a7e6 | |||
| 8f7f23c938 | |||
| cd2ce34f10 | |||
| d172f2e6d9 | |||
| 46517e4397 | |||
| b7ce5a8318 | |||
| 87d0a6bdf6 | |||
| b6ffe935e8 | |||
| 0673374e97 | |||
| 84c95f5a9d | |||
| 72401759b5 | |||
| 4e19fec123 | |||
| 5ce98fd83d | |||
| bc20ef73bd | |||
| 16d60a6586 | |||
| 0a5200766e | |||
| be937ec581 | |||
| 1f82b9fb78 | |||
| c8a1118679 | |||
| 91053ca51d | |||
| 4085771602 | |||
| efb000529b | |||
| 4b22200708 | |||
| f9148c36db | |||
| 7a82e7b365 |
@@ -203,6 +203,7 @@ dotnet_diagnostic.CA1819.severity = suggestion
|
|||||||
dotnet_diagnostic.CA1822.severity = suggestion
|
dotnet_diagnostic.CA1822.severity = suggestion
|
||||||
dotnet_diagnostic.CA1823.severity = suggestion
|
dotnet_diagnostic.CA1823.severity = suggestion
|
||||||
dotnet_diagnostic.CA1824.severity = suggestion
|
dotnet_diagnostic.CA1824.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA1825.severity = suggestion
|
||||||
dotnet_diagnostic.CA2000.severity = suggestion
|
dotnet_diagnostic.CA2000.severity = suggestion
|
||||||
dotnet_diagnostic.CA2002.severity = suggestion
|
dotnet_diagnostic.CA2002.severity = suggestion
|
||||||
dotnet_diagnostic.CA2007.severity = suggestion
|
dotnet_diagnostic.CA2007.severity = suggestion
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Sonarr v2 is EOL & unsupported.'
|
description: 'Only bug reports for v4 will be accepted, older versions are no longer receiving bug fixes and support issues will be closed immediately.'
|
||||||
labels: ['needs-triage']
|
labels: ['needs-triage']
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
@@ -37,8 +37,8 @@ body:
|
|||||||
label: Environment
|
label: Environment
|
||||||
description: |
|
description: |
|
||||||
examples:
|
examples:
|
||||||
- **OS**: Ubuntu 20.04
|
- **OS**: Ubuntu 22.04
|
||||||
- **Sonarr**: Sonarr 3.0.6.1265
|
- **Sonarr**: Sonarr 4.0.0.766
|
||||||
- **Docker Install**: Yes
|
- **Docker Install**: Yes
|
||||||
- **Using Reverse Proxy**: No
|
- **Using Reverse Proxy**: No
|
||||||
- **Browser**: Firefox 90 (If UI related)
|
- **Browser**: Firefox 90 (If UI related)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
#### Database Migration
|
|
||||||
YES | NO
|
|
||||||
|
|
||||||
#### Description
|
#### Description
|
||||||
A few sentences describing the overall goals of the pull request's commits.
|
A few sentences describing the overall goals of the pull request's commits.
|
||||||
|
|
||||||
#### Todos
|
<!-- Remove any of the following sections if they are not used -->
|
||||||
- [ ] Tests
|
|
||||||
- [ ] Wiki Updates
|
#### Screenshots for UI Changes
|
||||||
|
|
||||||
|
|
||||||
|
#### Database Migration
|
||||||
|
YES - ###
|
||||||
|
|
||||||
|
|
||||||
#### Issues Fixed or Closed by this PR
|
#### Issues Fixed or Closed by this PR
|
||||||
|
* Closes #
|
||||||
|
|
||||||
*
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Archive
|
||||||
|
description: Archive binaries for deployment
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
os:
|
||||||
|
description: 'OS that the packaging is running on'
|
||||||
|
required: true
|
||||||
|
artifact:
|
||||||
|
description: 'Binary artifact'
|
||||||
|
required: true
|
||||||
|
archive_type:
|
||||||
|
description: 'File type to use for the final package'
|
||||||
|
required: true
|
||||||
|
branch:
|
||||||
|
description: 'Git branch used for this build'
|
||||||
|
required: true
|
||||||
|
major_version:
|
||||||
|
description: 'Sonarr major version'
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description: 'Sonarr version'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Archive Artifact
|
||||||
|
uses: thedoctor0/zip-release@0.7.5
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: Package
|
||||||
|
description: Packages binaries for deployment
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
platform:
|
||||||
|
description: 'Binary platform'
|
||||||
|
required: true
|
||||||
|
framework:
|
||||||
|
description: '.net framework'
|
||||||
|
required: true
|
||||||
|
artifact:
|
||||||
|
description: 'Binary artifact'
|
||||||
|
required: true
|
||||||
|
branch:
|
||||||
|
description: 'Git branch used for this build'
|
||||||
|
required: true
|
||||||
|
major_version:
|
||||||
|
description: 'Sonarr major version'
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description: 'Sonarr version'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact }}
|
||||||
|
path: _output
|
||||||
|
|
||||||
|
- name: Download UI Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_ui
|
||||||
|
path: _output/UI
|
||||||
|
|
||||||
|
- name: Configure Environment Variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "FRAMEWORK=${{ inputs.framework }}" >> "$GITHUB_ENV"
|
||||||
|
echo "BRANCH=${{ inputs.branch }}" >> "$GITHUB_ENV"
|
||||||
|
echo "SONARR_MAJOR_VERSION=${{ inputs.major_version }}" >> "$GITHUB_ENV"
|
||||||
|
echo "SONARR_VERSION=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Create Packages
|
||||||
|
shell: bash
|
||||||
|
run: $GITHUB_ACTION_PATH/package.sh
|
||||||
|
|
||||||
|
- name: Create Windows Installer (x64)
|
||||||
|
if: ${{ inputs.platform == 'windows' }}
|
||||||
|
working-directory: distribution/windows/setup
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
SET RUNTIME=win-x64
|
||||||
|
|
||||||
|
build.bat
|
||||||
|
|
||||||
|
- name: Create Windows Installer (x86)
|
||||||
|
if: ${{ inputs.platform == 'windows' }}
|
||||||
|
working-directory: distribution/windows/setup
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
SET RUNTIME=win-x86
|
||||||
|
|
||||||
|
build.bat
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release_${{ inputs.platform }}
|
||||||
|
compression-level: 0
|
||||||
|
if-no-files-found: error
|
||||||
|
path: |
|
||||||
|
_artifacts/*.exe
|
||||||
|
_artifacts/*.tar.gz
|
||||||
|
_artifacts/*.zip
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
outputFolder=_output
|
||||||
|
artifactsFolder=_artifacts
|
||||||
|
uiFolder="$outputFolder/UI"
|
||||||
|
framework="${FRAMEWORK:=net6.0}"
|
||||||
|
|
||||||
|
rm -rf $artifactsFolder
|
||||||
|
mkdir $artifactsFolder
|
||||||
|
|
||||||
|
for runtime in _output/*
|
||||||
|
do
|
||||||
|
name="${runtime##*/}"
|
||||||
|
folderName="$runtime/$framework"
|
||||||
|
sonarrFolder="$folderName/Sonarr"
|
||||||
|
archiveName="Sonarr.$BRANCH.$SONARR_VERSION.$name"
|
||||||
|
|
||||||
|
if [[ "$name" == 'UI' ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating package for $name"
|
||||||
|
|
||||||
|
echo "Copying UI"
|
||||||
|
cp -r $uiFolder $sonarrFolder
|
||||||
|
|
||||||
|
echo "Setting permissions"
|
||||||
|
find $sonarrFolder -name "ffprobe" -exec chmod a+x {} \;
|
||||||
|
find $sonarrFolder -name "Sonarr" -exec chmod a+x {} \;
|
||||||
|
find $sonarrFolder -name "Sonarr.Update" -exec chmod a+x {} \;
|
||||||
|
|
||||||
|
if [[ "$name" == *"osx"* ]]; then
|
||||||
|
echo "Creating macOS package"
|
||||||
|
|
||||||
|
packageName="$name-app"
|
||||||
|
packageFolder="$outputFolder/$packageName"
|
||||||
|
|
||||||
|
rm -rf $packageFolder
|
||||||
|
mkdir $packageFolder
|
||||||
|
|
||||||
|
cp -r distribution/macOS/Sonarr.app $packageFolder
|
||||||
|
mkdir -p $packageFolder/Sonarr.app/Contents/MacOS
|
||||||
|
|
||||||
|
echo "Copying Binaries"
|
||||||
|
cp -r $sonarrFolder/* $packageFolder/Sonarr.app/Contents/MacOS
|
||||||
|
|
||||||
|
echo "Removing Update Folder"
|
||||||
|
rm -r $packageFolder/Sonarr.app/Contents/MacOS/Sonarr.Update
|
||||||
|
|
||||||
|
echo "Packaging macOS app Artifact"
|
||||||
|
(cd $packageFolder; zip -rq "../../$artifactsFolder/$archiveName-app.zip" ./Sonarr.app)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Packaging Artifact"
|
||||||
|
if [[ "$name" == *"linux"* ]] || [[ "$name" == *"osx"* ]] || [[ "$name" == *"freebsd"* ]]; then
|
||||||
|
tar -zcf "./$artifactsFolder/$archiveName.tar.gz" -C $folderName Sonarr
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$name" == *"win"* ]]; then
|
||||||
|
if [ "$RUNNER_OS" = "Windows" ]
|
||||||
|
then
|
||||||
|
(cd $folderName; 7z a -tzip "../../../$artifactsFolder/$archiveName.zip" ./Sonarr)
|
||||||
|
else
|
||||||
|
(cd $folderName; zip -rq "../../../$artifactsFolder/$archiveName.zip" ./Sonarr)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Publish Test Artifact
|
||||||
|
description: Publishes a test artifact
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
framework:
|
||||||
|
description: '.net framework'
|
||||||
|
required: true
|
||||||
|
runtime:
|
||||||
|
description: '.net runtime'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tests-${{ inputs.runtime }}
|
||||||
|
path: _tests/${{ inputs.framework }}/${{ inputs.runtime }}/publish/**/*
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
name: Test
|
||||||
|
description: Runs unit/integration tests
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
use_postgres:
|
||||||
|
description: 'Whether postgres should be used for the database'
|
||||||
|
os:
|
||||||
|
description: 'OS that the tests are running on'
|
||||||
|
required: true
|
||||||
|
artifact:
|
||||||
|
description: 'Test binary artifact'
|
||||||
|
required: true
|
||||||
|
pattern:
|
||||||
|
description: 'Pattern for DLLs'
|
||||||
|
required: true
|
||||||
|
filter:
|
||||||
|
description: 'Filter for tests'
|
||||||
|
required: true
|
||||||
|
integration_tests:
|
||||||
|
description: 'True if running integration tests'
|
||||||
|
binary_artifact:
|
||||||
|
description: 'Binary artifact for integration tests'
|
||||||
|
binary_path:
|
||||||
|
description: 'Path witin binary artifact for integration tests'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
|
||||||
|
- name: Setup Postgres
|
||||||
|
if: ${{ inputs.use_postgres }}
|
||||||
|
uses: ikalnytskyi/action-setup-postgres@v4
|
||||||
|
|
||||||
|
- name: Setup Test Variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "RESULTS_NAME=${{ inputs.integration_tests && 'integation-' || 'unit-' }}${{ inputs.artifact }}${{ inputs.use_postgres && '-postgres' }}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Setup Postgres Environment Variables
|
||||||
|
if: ${{ inputs.use_postgres }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Sonarr__Postgres__Host=localhost" >> "$GITHUB_ENV"
|
||||||
|
echo "Sonarr__Postgres__Port=5432" >> "$GITHUB_ENV"
|
||||||
|
echo "Sonarr__Postgres__User=postgres" >> "$GITHUB_ENV"
|
||||||
|
echo "Sonarr__Postgres__Password=postgres" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact }}
|
||||||
|
path: _tests
|
||||||
|
|
||||||
|
- name: Download Binary Artifact
|
||||||
|
if: ${{ inputs.integration_tests }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.binary_artifact }}
|
||||||
|
path: _output
|
||||||
|
|
||||||
|
- name: Set up binary artifact
|
||||||
|
if: ${{ inputs.binary_path != '' }}
|
||||||
|
shell: bash
|
||||||
|
run: mv ./_output/${{inputs.binary_path}} _tests/bin
|
||||||
|
|
||||||
|
- name: Make executable
|
||||||
|
if: startsWith(inputs.os, 'windows') != true
|
||||||
|
shell: bash
|
||||||
|
run: chmod +x ./_tests/Sonarr.Test.Dummy && chmod +x ./_tests/ffprobe
|
||||||
|
|
||||||
|
- name: Make Sonarr binary executable
|
||||||
|
if: ${{ inputs.integration_tests && !startsWith(inputs.os, 'windows') }}
|
||||||
|
shell: bash
|
||||||
|
run: chmod +x ./_tests/bin/Sonarr
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx" --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: results-${{ env.RESULTS_NAME }}
|
||||||
|
path: TestResults/*.trx
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
authors:
|
||||||
|
- Weblate
|
||||||
|
- SonarrBot
|
||||||
|
categories:
|
||||||
|
- title: Changes
|
||||||
|
labels:
|
||||||
|
- '*'
|
||||||
@@ -26,17 +26,11 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
id: setup-dotnet
|
id: setup-dotnet
|
||||||
with:
|
|
||||||
dotnet-version: '6.0.x'
|
|
||||||
|
|
||||||
- name: Create temporary global.json
|
|
||||||
run: |
|
|
||||||
echo '{"sdk":{"version": "${{ steps.setup-dotnet.outputs.dotnet-version }}" } }' > ./global.json
|
|
||||||
|
|
||||||
- name: Create openapi.json
|
- name: Create openapi.json
|
||||||
run: ./docs.sh Linux
|
run: ./docs.sh Linux
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- 'src/Sonarr.Api.*/openapi.json'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths-ignore:
|
||||||
|
- 'src/NzbDrone.Core/Localization/Core/**'
|
||||||
|
- 'src/Sonarr.Api.*/openapi.json'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
FRAMEWORK: net6.0
|
||||||
|
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
|
SONARR_MAJOR_VERSION: 4
|
||||||
|
VERSION: 4.0.2
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend:
|
||||||
|
runs-on: windows-latest
|
||||||
|
outputs:
|
||||||
|
framework: ${{ steps.variables.outputs.framework }}
|
||||||
|
major_version: ${{ steps.variables.outputs.major_version }}
|
||||||
|
version: ${{ steps.variables.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
|
||||||
|
- name: Setup Environment Variables
|
||||||
|
id: variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Add 800 to the build number because GitHub won't let us pick an arbitrary starting point
|
||||||
|
SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))"
|
||||||
|
DOTNET_VERSION=$(jq -r '.sdk.version' global.json)
|
||||||
|
|
||||||
|
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
|
||||||
|
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
|
||||||
|
echo "BRANCH=${RAW_BRANCH_NAME/\//-}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Enable Extra Platforms In SDK
|
||||||
|
shell: bash
|
||||||
|
run: ./build.sh --enable-extra-platforms-in-sdk
|
||||||
|
|
||||||
|
- name: Build Backend
|
||||||
|
shell: bash
|
||||||
|
run: ./build.sh --backend --enable-extra-platforms --packages
|
||||||
|
|
||||||
|
# Test Artifacts
|
||||||
|
|
||||||
|
- name: Publish win-x64 Test Artifact
|
||||||
|
uses: ./.github/actions/publish-test-artifact
|
||||||
|
with:
|
||||||
|
framework: ${{ env.FRAMEWORK }}
|
||||||
|
runtime: win-x64
|
||||||
|
|
||||||
|
- name: Publish linux-x64 Test Artifact
|
||||||
|
uses: ./.github/actions/publish-test-artifact
|
||||||
|
with:
|
||||||
|
framework: ${{ env.FRAMEWORK }}
|
||||||
|
runtime: linux-x64
|
||||||
|
|
||||||
|
- name: Publish osx-x64 Test Artifact
|
||||||
|
uses: ./.github/actions/publish-test-artifact
|
||||||
|
with:
|
||||||
|
framework: ${{ env.FRAMEWORK }}
|
||||||
|
runtime: osx-x64
|
||||||
|
|
||||||
|
# Build Artifacts (grouped by OS)
|
||||||
|
|
||||||
|
- name: Publish FreeBSD Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_freebsd
|
||||||
|
path: _artifacts/freebsd-*/**/*
|
||||||
|
- name: Publish Linux Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_linux
|
||||||
|
path: _artifacts/linux-*/**/*
|
||||||
|
- name: Publish macOS Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_macos
|
||||||
|
path: _artifacts/osx-*/**/*
|
||||||
|
- name: Publish Windows Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_windows
|
||||||
|
path: _artifacts/win-*/**/*
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Volta
|
||||||
|
uses: volta-cli/action@v4
|
||||||
|
|
||||||
|
- name: Yarn Install
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
- name: Stylelint
|
||||||
|
run: yarn stylelint
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build --env production
|
||||||
|
|
||||||
|
- name: Publish UI Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_ui
|
||||||
|
path: _output/UI/**/*
|
||||||
|
|
||||||
|
unit_test:
|
||||||
|
needs: backend
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact: tests-linux-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
- os: macos-latest
|
||||||
|
artifact: tests-osx-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
- os: windows-latest
|
||||||
|
artifact: tests-win-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
with:
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
artifact: ${{ matrix.artifact }}
|
||||||
|
pattern: Sonarr.*.Test.dll
|
||||||
|
filter: ${{ matrix.filter }}
|
||||||
|
|
||||||
|
unit_test_postgres:
|
||||||
|
needs: backend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
with:
|
||||||
|
os: ubuntu-latest
|
||||||
|
artifact: tests-linux-x64
|
||||||
|
pattern: Sonarr.*.Test.dll
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
use_postgres: true
|
||||||
|
|
||||||
|
integration_test:
|
||||||
|
needs: backend
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact: tests-linux-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
|
||||||
|
binary_artifact: build_linux
|
||||||
|
binary_path: linux-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||||
|
- os: macos-latest
|
||||||
|
artifact: tests-osx-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
|
||||||
|
binary_artifact: build_macos
|
||||||
|
binary_path: osx-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||||
|
- os: windows-latest
|
||||||
|
artifact: tests-win-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest
|
||||||
|
binary_artifact: build_windows
|
||||||
|
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
with:
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
artifact: ${{ matrix.artifact }}
|
||||||
|
pattern: Sonarr.*.Test.dll
|
||||||
|
filter: ${{ matrix.filter }}
|
||||||
|
integration_tests: true
|
||||||
|
binary_artifact: ${{ matrix.binary_artifact }}
|
||||||
|
binary_path: ${{ matrix.binary_path }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
|
||||||
|
needs: [backend, unit_test, unit_test_postgres, integration_test]
|
||||||
|
secrets: inherit
|
||||||
|
uses: ./.github/workflows/deploy.yml
|
||||||
|
with:
|
||||||
|
framework: ${{ needs.backend.outputs.framework }}
|
||||||
|
branch: ${{ github.ref_name }}
|
||||||
|
major_version: ${{ needs.backend.outputs.major_version }}
|
||||||
|
version: ${{ needs.backend.outputs.version }}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
framework:
|
||||||
|
description: '.net framework'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
branch:
|
||||||
|
description: 'Git branch used for this build'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
major_version:
|
||||||
|
description: 'Sonarr major version'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description: 'Sonarr version'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
secrets:
|
||||||
|
SERVICES_API_KEY:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [freebsd, linux, macos, windows]
|
||||||
|
include:
|
||||||
|
- platform: freebsd
|
||||||
|
os: ubuntu-latest
|
||||||
|
- platform: linux
|
||||||
|
os: ubuntu-latest
|
||||||
|
- platform: macos
|
||||||
|
os: ubuntu-latest
|
||||||
|
- platform: windows
|
||||||
|
os: windows-latest
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
uses: ./.github/actions/package
|
||||||
|
with:
|
||||||
|
framework: ${{ inputs.framework }}
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
artifact: build_${{ matrix.platform }}
|
||||||
|
branch: ${{ inputs.branch }}
|
||||||
|
major_version: ${{ inputs.major_version }}
|
||||||
|
version: ${{ inputs.version }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download release artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: _artifacts
|
||||||
|
pattern: release_*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
artifacts: _artifacts/Sonarr.*
|
||||||
|
commit: ${{ github.sha }}
|
||||||
|
generateReleaseNotes: true
|
||||||
|
name: ${{ inputs.version }}
|
||||||
|
prerelease: ${{ inputs.branch != 'main' }}
|
||||||
|
skipIfReleaseExists: true
|
||||||
|
tag: v${{ inputs.version }}
|
||||||
|
|
||||||
|
- name: Publish to Services
|
||||||
|
shell: bash
|
||||||
|
working-directory: _artifacts
|
||||||
|
run: |
|
||||||
|
branch=${{ inputs.branch }}
|
||||||
|
version=${{ inputs.version }}
|
||||||
|
lastCommit=${{ github.sha }}
|
||||||
|
|
||||||
|
hashes="["
|
||||||
|
|
||||||
|
addHash() {
|
||||||
|
path=$1
|
||||||
|
os=$2
|
||||||
|
arch=$3
|
||||||
|
type=$4
|
||||||
|
|
||||||
|
local hash=$(sha256sum *.$version.$path | awk '{ print $1; }')
|
||||||
|
echo "{ \""Os\"": \""$os\"", \""Arch\"": \""$arch\"", \""Type\"": \""$type\"", \""Hash\"": \""$hash\"" }"
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes="$hashes $(addHash "linux-arm.tar.gz" "linux" "arm" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-arm64.tar.gz" "linux" "arm64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-x64.tar.gz" "linux" "x64" "archive")"
|
||||||
|
# hashes="$hashes, $(addHash "linux-x86.tar.gz" "linux" "x86" "archive")"
|
||||||
|
|
||||||
|
# hashes="$hashes, $(addHash "linux-musl-arm.tar.gz" "linuxmusl" "arm" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-musl-arm64.tar.gz" "linuxmusl" "arm64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-musl-x64.tar.gz" "linuxmusl" "x64" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "osx-arm64.tar.gz" "osx" "arm64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "osx-x64.tar.gz" "osx" "x64" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "osx-arm64-app.zip" "osx" "arm64" "installer")"
|
||||||
|
hashes="$hashes, $(addHash "osx-x64-app.zip" "osx" "x64" "installer")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "win-x64.zip" "windows" "x64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "win-x86.zip" "windows" "x86" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "win-x64-installer.exe" "windows" "x64" "installer")"
|
||||||
|
hashes="$hashes, $(addHash "win-x86-installer.exe" "windows" "x86" "installer")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "freebsd-x64.tar.gz" "freebsd" "x64" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes ]"
|
||||||
|
|
||||||
|
json="{\""branch\"":\""$branch\"", \""version\"":\""$version\"", \""lastCommit\"":\""$lastCommit\"", \""hashes\"":$hashes, \""gitHubRelease\"":true}"
|
||||||
|
url="https://services.sonarr.tv/v1/update"
|
||||||
|
|
||||||
|
echo "Publishing update $version ($branch) to: $url"
|
||||||
|
echo "$json"
|
||||||
|
|
||||||
|
curl -H "Content-Type: application/json" -H "X-Api-Key: ${{ secrets.SERVICES_API_KEY }}" -X POST -d "$json" --fail-with-body $url
|
||||||
@@ -4,19 +4,18 @@ set -e
|
|||||||
outputFolder='_output'
|
outputFolder='_output'
|
||||||
testPackageFolder='_tests'
|
testPackageFolder='_tests'
|
||||||
artifactsFolder="_artifacts";
|
artifactsFolder="_artifacts";
|
||||||
|
framework="${FRAMEWORK:=net6.0}"
|
||||||
|
|
||||||
ProgressStart()
|
ProgressStart()
|
||||||
{
|
{
|
||||||
echo "##teamcity[blockOpened name='$1']"
|
echo "::group::$1"
|
||||||
echo "##teamcity[progressStart '$1']"
|
|
||||||
echo "Start '$1'"
|
echo "Start '$1'"
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressEnd()
|
ProgressEnd()
|
||||||
{
|
{
|
||||||
echo "Finish '$1'"
|
echo "Finish '$1'"
|
||||||
echo "##teamcity[progressFinish '$1']"
|
echo "::endgroup::"
|
||||||
echo "##teamcity[blockClosed name='$1']"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateVersionNumber()
|
UpdateVersionNumber()
|
||||||
@@ -140,7 +139,7 @@ PackageLinux()
|
|||||||
|
|
||||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||||
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "$framework" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
||||||
fi
|
fi
|
||||||
@@ -168,7 +167,7 @@ PackageMacOS()
|
|||||||
|
|
||||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||||
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "$framework" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
||||||
fi
|
fi
|
||||||
@@ -400,20 +399,20 @@ then
|
|||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "win-x64"
|
PackageTests "$framework" "win-x64"
|
||||||
PackageTests "net6.0" "win-x86"
|
PackageTests "$framework" "win-x86"
|
||||||
PackageTests "net6.0" "linux-x64"
|
PackageTests "$framework" "linux-x64"
|
||||||
PackageTests "net6.0" "linux-musl-x64"
|
PackageTests "$framework" "linux-musl-x64"
|
||||||
PackageTests "net6.0" "osx-x64"
|
PackageTests "$framework" "osx-x64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "freebsd-x64"
|
PackageTests "$framework" "freebsd-x64"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PackageTests "$FRAMEWORK" "$RID"
|
PackageTests "$FRAMEWORK" "$RID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
UploadTestArtifacts "net6.0"
|
UploadTestArtifacts "$framework"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$FRONTEND" = "YES" ];
|
if [ "$FRONTEND" = "YES" ];
|
||||||
@@ -435,22 +434,22 @@ then
|
|||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
Package "net6.0" "win-x64"
|
Package "$framework" "win-x64"
|
||||||
Package "net6.0" "win-x86"
|
Package "$framework" "win-x86"
|
||||||
Package "net6.0" "linux-x64"
|
Package "$framework" "linux-x64"
|
||||||
Package "net6.0" "linux-musl-x64"
|
Package "$framework" "linux-musl-x64"
|
||||||
Package "net6.0" "linux-arm64"
|
Package "$framework" "linux-arm64"
|
||||||
Package "net6.0" "linux-musl-arm64"
|
Package "$framework" "linux-musl-arm64"
|
||||||
Package "net6.0" "linux-arm"
|
Package "$framework" "linux-arm"
|
||||||
Package "net6.0" "osx-x64"
|
Package "$framework" "osx-x64"
|
||||||
Package "net6.0" "osx-arm64"
|
Package "$framework" "osx-arm64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
Package "net6.0" "freebsd-x64"
|
Package "$framework" "freebsd-x64"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
Package "$FRAMEWORK" "$RID"
|
Package "$FRAMEWORK" "$RID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
UploadArtifacts "net6.0"
|
UploadArtifacts "$framework"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
fromdos ./debian/*
|
|
||||||
chmod ugo-x ./debian/*
|
|
||||||
cp -r ./debian ./debian_backup
|
|
||||||
|
|
||||||
BuildVersion=${dependent_build_number:-4.10.0.999}
|
|
||||||
BuildBranch=${dependent_build_branch:-main}
|
|
||||||
BootstrapVersion=`echo "$BuildVersion" | cut -d. -f1,2,3`
|
|
||||||
BootstrapUpdater="BuiltIn"
|
|
||||||
PackageUpdater="apt"
|
|
||||||
|
|
||||||
echo Version: "$BuildVersion" Branch: "$BuildBranch"
|
|
||||||
|
|
||||||
rm -r ./sonarr_bin/Sonarr.Update
|
|
||||||
chmod -R ugo-x,ugo+rwX,go-w ./sonarr_bin/*
|
|
||||||
|
|
||||||
echo Updating changelog for $BuildVersion
|
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$PackageUpdater:g" debian/preinst debian/postinst debian/postrm
|
|
||||||
sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
|
||||||
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
|
||||||
|
|
||||||
echo Running debuild for $BuildVersion
|
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
|
||||||
debuild -b
|
|
||||||
else
|
|
||||||
debuild -us -uc -b
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore debian directory to the original files
|
|
||||||
rm -rf ./debian
|
|
||||||
mv ./debian_backup ./debian
|
|
||||||
|
|
||||||
echo Updating changelog for $BootstrapVersion
|
|
||||||
sed -i "s:{version}:$BootstrapVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$BootstrapUpdater:g" debian/preinst debian/postinst debian/postrm
|
|
||||||
sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
|
||||||
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
|
||||||
|
|
||||||
echo Running debuild for $BootstrapVersion
|
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
|
||||||
debuild -b
|
|
||||||
else
|
|
||||||
debuild -us -uc -b
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo Moving stuff around
|
|
||||||
mv ../sonarr_*.deb ./
|
|
||||||
mv ../sonarr_*.changes ./
|
|
||||||
rm ../sonarr_*.build
|
|
||||||
|
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
|
||||||
echo Signing Package
|
|
||||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
|
||||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
|
||||||
|
|
||||||
echo running alien
|
|
||||||
alien -r -v ./*.deb
|
|
||||||
else
|
|
||||||
echo "Exporting packages to ${TEST_OUTPUT}"
|
|
||||||
dpkg -e "sonarr_${BuildVersion}_all.deb" ${TEST_OUTPUT}/sonarr-build
|
|
||||||
dpkg -e "sonarr_${BootstrapVersion}_all.deb" ${TEST_OUTPUT}/sonarr-release
|
|
||||||
|
|
||||||
cp *.deb ${TEST_OUTPUT}/
|
|
||||||
fi
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
sonarr ({version}) {branch}; urgency=low
|
|
||||||
|
|
||||||
* Automatic Release.
|
|
||||||
|
|
||||||
-- Sonarr <hello@sonarr.tv> Sun, 28 Jan 2018 00:00:00 -0700
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
10
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh -e
|
|
||||||
|
|
||||||
. /usr/share/debconf/confmodule
|
|
||||||
|
|
||||||
db_beginblock
|
|
||||||
db_input high sonarr/owning_user || true
|
|
||||||
db_input high sonarr/owning_group || true
|
|
||||||
db_endblock
|
|
||||||
db_go
|
|
||||||
|
|
||||||
db_beginblock
|
|
||||||
db_input low sonarr/owning_umask || true
|
|
||||||
db_input low sonarr/config_directory || true
|
|
||||||
db_endblock
|
|
||||||
db_go
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Sonarr <hello@sonarr.tv>
|
|
||||||
Source: sonarr
|
|
||||||
Homepage: https://sonarr.tv
|
|
||||||
Vcs-Git: git@github.com:Sonarr/Sonarr.git
|
|
||||||
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
|
||||||
Build-Depends: debhelper (>= 9),
|
|
||||||
dh-systemd (>= 1.5)
|
|
||||||
|
|
||||||
Package: sonarr
|
|
||||||
Architecture: all
|
|
||||||
Provides: nzbdrone
|
|
||||||
Conflicts: nzbdrone
|
|
||||||
Replaces: nzbdrone
|
|
||||||
Depends: adduser, libsqlite3-0 (>= 3.7), ${cli:Depends}, ${misc:Depends}
|
|
||||||
Suggests: sqlite3 (>= 3.7)
|
|
||||||
Description: Internet PVR
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: sonarr
|
|
||||||
Source: https://github.com/Sonarr/Sonarr
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2010-2016 Sonarr <hello@sonarr.tv>
|
|
||||||
|
|
||||||
License: GPL-3.0+
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
.
|
|
||||||
This package is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
.
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
.
|
|
||||||
On Debian systems, the complete text of the GNU General
|
|
||||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
sonarr_3.0.0.0_all.deb web optional
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
sonarr_bin/* usr/lib/sonarr/bin
|
|
||||||
package_info usr/lib/sonarr
|
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
### Description: Sonarr .NET Debian install
|
||||||
|
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
|
||||||
|
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
|
||||||
|
### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Sonarr installs
|
||||||
|
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
|
||||||
|
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
|
||||||
|
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
|
||||||
|
|
||||||
|
### Boilerplate Warning
|
||||||
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
scriptversion="1.0.3"
|
||||||
|
scriptdate="2024-01-06"
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Running Sonarr Install Script - Version [$scriptversion] as of [$scriptdate]"
|
||||||
|
|
||||||
|
# Am I root?, need root!
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Please run as root."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
app="sonarr"
|
||||||
|
app_port="8989"
|
||||||
|
app_prereq="curl sqlite3 wget"
|
||||||
|
app_umask="0002"
|
||||||
|
branch="main"
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
### Update these variables as required for your specific instance
|
||||||
|
installdir="/opt" # {Update me if needed} Install Location
|
||||||
|
bindir="${installdir}/${app^}" # Full Path to Install Location
|
||||||
|
datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use
|
||||||
|
app_bin=${app^} # Binary Name of the app
|
||||||
|
|
||||||
|
# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir.
|
||||||
|
if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then
|
||||||
|
echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt User
|
||||||
|
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
|
||||||
|
app_uid=$(echo "$app_uid" | tr -d ' ')
|
||||||
|
app_uid=${app_uid:-$app}
|
||||||
|
# Prompt Group
|
||||||
|
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
|
||||||
|
app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||||
|
app_guid=${app_guid:-media}
|
||||||
|
|
||||||
|
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||||
|
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||||
|
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||||
|
|
||||||
|
# Create User / Group as needed
|
||||||
|
if [ "$app_guid" != "$app_uid" ]; then
|
||||||
|
if ! getent group "$app_guid" >/dev/null; then
|
||||||
|
groupadd "$app_guid"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! getent passwd "$app_uid" >/dev/null; then
|
||||||
|
adduser --system --no-create-home --ingroup "$app_guid" "$app_uid"
|
||||||
|
echo "Created and added User [$app_uid] to Group [$app_guid]"
|
||||||
|
fi
|
||||||
|
if ! getent group "$app_guid" | grep -qw "$app_uid"; then
|
||||||
|
echo "User [$app_uid] did not exist in Group [$app_guid]"
|
||||||
|
usermod -a -G "$app_guid" "$app_uid"
|
||||||
|
echo "Added User [$app_uid] to Group [$app_guid]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop the App if running
|
||||||
|
if service --status-all | grep -Fq "$app"; then
|
||||||
|
systemctl stop "$app"
|
||||||
|
systemctl disable "$app".service
|
||||||
|
echo "Stopped existing $app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create Appdata Directory
|
||||||
|
|
||||||
|
# AppData
|
||||||
|
mkdir -p "$datadir"
|
||||||
|
chown -R "$app_uid":"$app_guid" "$datadir"
|
||||||
|
chmod 775 "$datadir"
|
||||||
|
echo "Directories created"
|
||||||
|
# Download and install the App
|
||||||
|
|
||||||
|
# prerequisite packages
|
||||||
|
echo ""
|
||||||
|
echo "Installing pre-requisite Packages"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
apt update && apt install -y $app_prereq
|
||||||
|
echo ""
|
||||||
|
ARCH=$(dpkg --print-architecture)
|
||||||
|
# get arch
|
||||||
|
dlbase="https://services.sonarr.tv/v1/download/$branch/latest?version=4&os=linux"
|
||||||
|
case "$ARCH" in
|
||||||
|
"amd64") DLURL="${dlbase}&arch=x64" ;;
|
||||||
|
"armhf") DLURL="${dlbase}&arch=arm" ;;
|
||||||
|
"arm64") DLURL="${dlbase}&arch=arm64" ;;
|
||||||
|
*)
|
||||||
|
echo "Arch not supported"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo ""
|
||||||
|
echo "Removing previous tarballs"
|
||||||
|
# -f to Force so we fail if it doesnt exist
|
||||||
|
rm -f "${app^}".*.tar.gz
|
||||||
|
echo ""
|
||||||
|
echo "Downloading..."
|
||||||
|
wget --content-disposition "$DLURL"
|
||||||
|
tar -xvzf "${app^}".*.tar.gz
|
||||||
|
echo ""
|
||||||
|
echo "Installation files downloaded and extracted"
|
||||||
|
|
||||||
|
# remove existing installs
|
||||||
|
echo "Removing existing installation"
|
||||||
|
rm -rf "$bindir"
|
||||||
|
echo "Installing..."
|
||||||
|
mv "${app^}" $installdir
|
||||||
|
chown "$app_uid":"$app_guid" -R "$bindir"
|
||||||
|
chmod 775 "$bindir"
|
||||||
|
rm -rf "${app^}.*.tar.gz"
|
||||||
|
# Ensure we check for an update in case user installs older version or different branch
|
||||||
|
touch "$datadir"/update_required
|
||||||
|
chown "$app_uid":"$app_guid" "$datadir"/update_required
|
||||||
|
echo "App Installed"
|
||||||
|
# Configure Autostart
|
||||||
|
|
||||||
|
# Remove any previous app .service
|
||||||
|
echo "Removing old service file"
|
||||||
|
rm -rf /etc/systemd/system/"$app".service
|
||||||
|
|
||||||
|
# Create app .service with correct user startup
|
||||||
|
echo "Creating service file"
|
||||||
|
cat <<EOF | tee /etc/systemd/system/"$app".service >/dev/null
|
||||||
|
[Unit]
|
||||||
|
Description=${app^} Daemon
|
||||||
|
After=syslog.target network.target
|
||||||
|
[Service]
|
||||||
|
User=$app_uid
|
||||||
|
Group=$app_guid
|
||||||
|
UMask=$app_umask
|
||||||
|
Type=simple
|
||||||
|
ExecStart=$bindir/$app_bin -nobrowser -data=$datadir
|
||||||
|
TimeoutStopSec=20
|
||||||
|
KillMode=process
|
||||||
|
Restart=on-failure
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Start the App
|
||||||
|
echo "Service file created. Attempting to start the app"
|
||||||
|
systemctl -q daemon-reload
|
||||||
|
systemctl enable --now -q "$app"
|
||||||
|
|
||||||
|
# Finish Update/Installation
|
||||||
|
host=$(hostname -I)
|
||||||
|
ip_local=$(grep -oP '^\S*' <<<"$host")
|
||||||
|
echo ""
|
||||||
|
echo "Install complete"
|
||||||
|
sleep 10
|
||||||
|
STATUS="$(systemctl is-active "$app")"
|
||||||
|
if [ "${STATUS}" = "active" ]; then
|
||||||
|
echo "Browse to http://$ip_local:$app_port for the ${app^} GUI"
|
||||||
|
else
|
||||||
|
echo "${app^} failed to start"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit
|
||||||
|
exit 0
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BUILD_VERSION={version}
|
|
||||||
UPDATER={updater}
|
|
||||||
|
|
||||||
. /usr/share/debconf/confmodule
|
|
||||||
db_get sonarr/owning_user
|
|
||||||
USER="$RET"
|
|
||||||
db_get sonarr/owning_group
|
|
||||||
GROUP="$RET"
|
|
||||||
db_get sonarr/owning_umask
|
|
||||||
UMASK="$RET"
|
|
||||||
db_get sonarr/config_directory
|
|
||||||
CONFDIR="$RET"
|
|
||||||
|
|
||||||
# Add User and Group
|
|
||||||
if ! getent group "$GROUP" >/dev/null; then
|
|
||||||
groupadd "$GROUP"
|
|
||||||
fi
|
|
||||||
if ! getent passwd "$USER" >/dev/null; then
|
|
||||||
adduser --system --no-create-home --ingroup "$GROUP" "$USER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $1 = "configure" ]; then
|
|
||||||
# Migrate old Sonarr v3 alpha data dir from /var/opt/sonarr or user home
|
|
||||||
if [ -d "/var/opt/sonarr" ] && [ "$CONFDIR" != "/var/opt/sonarr" ] && [ ! -d "$CONFDIR" ]; then
|
|
||||||
varoptRoot="/var/opt/sonarr"
|
|
||||||
varoptAppData="$varoptRoot/.config/Sonarr"
|
|
||||||
sonarrUserHome=`getent passwd $USER | cut -d ':' -f 6`
|
|
||||||
sonarrAppData="$sonarrUserHome/.config/Sonarr"
|
|
||||||
if [ -f "$varoptRoot/sonarr.db" ]; then
|
|
||||||
# Handle /var/opt/sonarr/sonarr.db
|
|
||||||
mv "$varoptRoot" "$CONFDIR"
|
|
||||||
elif [ -f "$varoptAppData/sonarr.db" ]; then
|
|
||||||
# Handle /var/opt/sonarr/.config/Sonarr/sonarr.db
|
|
||||||
mv "$varoptAppData" "$CONFDIR"
|
|
||||||
rm -rf "$varoptRoot"
|
|
||||||
elif [ -f "$sonarrAppData/sonarr.db" ]; then
|
|
||||||
# Handle ~/.config/Sonarr/sonarr.db
|
|
||||||
mv "$sonarrAppData" "$CONFDIR"
|
|
||||||
rm -rf "$sonarrAppData"
|
|
||||||
else
|
|
||||||
mv "$varoptRoot" "$CONFDIR"
|
|
||||||
fi
|
|
||||||
chown -R $USER:$GROUP "$CONFDIR"
|
|
||||||
chmod -R 775 "$CONFDIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Migrate old NzbDrone data dir
|
|
||||||
if [ -d "/usr/lib/sonarr/nzbdrone-appdata" ] && [ ! -d "$CONFDIR" ]; then
|
|
||||||
NZBDRONE_DATA=`readlink /usr/lib/sonarr/nzbdrone-appdata`
|
|
||||||
if [ -f "$NZBDRONE_DATA/config.xml" ] && [ -f "$NZBDRONE_DATA/nzbdrone.db" ]; then
|
|
||||||
echo "Found NzbDrone database in $NZBDRONE_DATA, copying to $CONFDIR."
|
|
||||||
mkdir -p "$CONFDIR"
|
|
||||||
cp $NZBDRONE_DATA/config.xml $NZBDRONE_DATA/nzbdrone.db* "$CONFDIR/"
|
|
||||||
chown -R $USER:$GROUP "$CONFDIR"
|
|
||||||
chmod -R 775 "$CONFDIR"
|
|
||||||
else
|
|
||||||
echo "Missing NzbDrone database in $NZBDRONE_DATA, skipping migration."
|
|
||||||
fi
|
|
||||||
rm /usr/lib/sonarr/nzbdrone-appdata
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create data directory
|
|
||||||
if [ ! -d "$CONFDIR" ]; then
|
|
||||||
mkdir -p "$CONFDIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set permissions on data directory (always do this instead only on creation in case user was changed via dpkg-reconfigure)
|
|
||||||
chown -R $USER:$GROUP "$CONFDIR"
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
# Apply patch if present
|
|
||||||
if [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin_patch/release_info ]; then
|
|
||||||
# It shouldn't be possible to get a wrong bin_patch, but let's check anyway and throw it away if it's wrong
|
|
||||||
currentVersion=`cat /usr/lib/sonarr/bin_patch/release_info | grep 'ReleaseVersion=' | cut -d= -f 2`
|
|
||||||
currentRelease=`echo "$currentVersion" | cut -d. -f1,2,3`
|
|
||||||
currentBuild=`echo "$currentVersion" | cut -d. -f4`
|
|
||||||
targetVersion=$BUILD_VERSION
|
|
||||||
targetRelease=`echo "$targetVersion" | cut -d. -f1,2,3`
|
|
||||||
targetBuild=`echo "$targetVersion" | cut -d. -f4`
|
|
||||||
|
|
||||||
if [ "$currentRelease" = "$targetRelease" ] && [ "$currentBuild" -gt "$targetBuild" ]; then
|
|
||||||
echo "Applying $currentVersion from BuiltIn updater instead of downgrading to $targetVersion"
|
|
||||||
rm -rf /usr/lib/sonarr/bin
|
|
||||||
mv /usr/lib/sonarr/bin_patch /usr/lib/sonarr/bin
|
|
||||||
else
|
|
||||||
rm -rf /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
# Set permissions on /usr/lib/sonarr
|
|
||||||
chown -R $USER:$GROUP /usr/lib/sonarr
|
|
||||||
|
|
||||||
# Update sonarr.service file
|
|
||||||
sed -i "s:User=\w*:User=$USER:g; s:Group=\w*:Group=$GROUP:g; s:UMask=[0-9]*:UMask=$UMASK:g; s:-data=.*$:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
if [ "$UPDATER" = "BuiltIn" ]; then
|
|
||||||
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
|
|
||||||
touch $CONFDIR/update_required
|
|
||||||
chown $USER:$GROUP $CONFDIR/update_required
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BUILD_VERSION={version}
|
|
||||||
UPDATER={updater}
|
|
||||||
|
|
||||||
if [ $1 = "abort-install" ]; then
|
|
||||||
# preinst was aborted, possibly due to NzbDrone still running.
|
|
||||||
# Nothing to do here
|
|
||||||
:
|
|
||||||
fi
|
|
||||||
|
|
||||||
# The bin directory is expected to be empty, unless the BuiltIn updater added files.
|
|
||||||
if [ $1 = "remove" ] && [ -d /usr/lib/sonarr/bin ]; then
|
|
||||||
rm -rf /usr/lib/sonarr/bin
|
|
||||||
fi
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
# Remove any existing patch if still present
|
|
||||||
if [ $1 = "remove" ] && [ -d /usr/lib/sonarr/bin_patch ]; then
|
|
||||||
rm -rf /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
# Purge the entire sonarr configuration directory.
|
|
||||||
# TODO: Maybe move a minimal backup to tmp?
|
|
||||||
if [ $1 = "purge" ] && [ -e /usr/share/debconf/confmodule ]; then
|
|
||||||
. /usr/share/debconf/confmodule
|
|
||||||
db_get sonarr/config_directory
|
|
||||||
CONFDIR="$RET"
|
|
||||||
if [ -d "$CONFDIR" ]; then
|
|
||||||
rm -rf "$CONFDIR"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BUILD_VERSION={version}
|
|
||||||
UPDATER={updater}
|
|
||||||
|
|
||||||
# Deal with existing nzbdrone installs
|
|
||||||
#
|
|
||||||
# Existing nzbdrone packages do not have startup scripts and the process might still be running.
|
|
||||||
# If the user manually installed nzbdrone then the process might still be running too.
|
|
||||||
if [ $1 = "install" ]; then
|
|
||||||
psNzbDrone=`ps ax -o'user:20,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
|
|
||||||
if [ ! -z "$psNzbDrone" ]; then
|
|
||||||
# Get the user and optional systemd unit
|
|
||||||
psNzbDroneUser=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 1`
|
|
||||||
psNzbDroneUnit=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 4`
|
|
||||||
# Get the appdata from the cmdline or get it from the user dir
|
|
||||||
droneAppData=`echo "$psNzbDrone" | tr ' ' '\n' | grep -- "-data=" | cut -d= -f 2`
|
|
||||||
if [ "$droneAppData" = "" ]; then
|
|
||||||
droneUserHome=`getent passwd $psNzbDroneUser | cut -d ':' -f 6`
|
|
||||||
droneAppData="$droneUserHome/.config/NzbDrone"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
|
|
||||||
if [ "$psNzbDroneUnit" = "sonarr.service" ]; then
|
|
||||||
# Conflicts with our new sonarr.service so we have to remove it
|
|
||||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and removing..."
|
|
||||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
|
||||||
if [ -f "/etc/systemd/system/$psNzbDroneUnit" ]; then
|
|
||||||
rm /etc/systemd/system/$psNzbDroneUnit
|
|
||||||
fi
|
|
||||||
if [ -f "/usr/lib/systemd/system/$psNzbDroneUnit" ]; then
|
|
||||||
rm /usr/lib/systemd/system/$psNzbDroneUnit
|
|
||||||
fi
|
|
||||||
deb-systemd-helper purge $psNzbDroneUnit >/dev/null
|
|
||||||
deb-systemd-helper unmask $psNzbDroneUnit >/dev/null
|
|
||||||
systemctl --system daemon-reload >/dev/null || true
|
|
||||||
else
|
|
||||||
# Just disable it, so the user can revisit the settings later
|
|
||||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
|
||||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
|
||||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# We don't support auto migration for other startup methods, so bail.
|
|
||||||
# This leaves the sonarr package in an incomplete state.
|
|
||||||
echo "ps: $psNzbDrone"
|
|
||||||
echo "Error: An existing Sonarr v2 (NzbDrone) process is running. Remove the NzbDrone auto-startup prior to installing sonarr."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# We don't have the debconf configuration yet so we can't migrate the data.
|
|
||||||
# Instead we symlink so postinst knows where it's at.
|
|
||||||
if [ -f "/usr/lib/sonarr/nzbdrone-appdata" ]; then
|
|
||||||
rm "/usr/lib/sonarr/nzbdrone-appdata"
|
|
||||||
else
|
|
||||||
mkdir -p "/usr/lib/sonarr"
|
|
||||||
fi
|
|
||||||
ln -s $droneAppData /usr/lib/sonarr/nzbdrone-appdata
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
# Check for supported upgrade paths
|
|
||||||
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin/release_info ]; then
|
|
||||||
# If we allow the Built-In updater to upgrade from 3.0.1.123 to 3.0.2.500 and now apt is catching up to 3.0.2.425
|
|
||||||
# then we need to deal with that 500->425 'downgrade'.
|
|
||||||
# We do that by preserving the binaries and using those instead for postinst.
|
|
||||||
|
|
||||||
currentVersion=`cat /usr/lib/sonarr/bin/release_info | grep 'ReleaseVersion=' | cut -d= -f 2`
|
|
||||||
currentRelease=`echo "$currentVersion" | cut -d. -f1,2,3`
|
|
||||||
currentBuild=`echo "$currentVersion" | cut -d. -f4`
|
|
||||||
targetVersion=$BUILD_VERSION
|
|
||||||
targetRelease=`echo "$targetVersion" | cut -d. -f1,2,3`
|
|
||||||
targetBuild=`echo "$targetVersion" | cut -d. -f4`
|
|
||||||
|
|
||||||
if [ -d /usr/lib/sonarr/bin_patch ]; then
|
|
||||||
rm -rf /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the existing version is already an upgrade for the included build
|
|
||||||
if [ "$currentRelease" = "$targetRelease" ] && [ "$currentBuild" -gt "$targetBuild" ]; then
|
|
||||||
echo "Preserving $currentVersion from BuiltIn updater instead of downgrading to $targetVersion"
|
|
||||||
cp -r /usr/lib/sonarr/bin /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/make -f
|
|
||||||
|
|
||||||
# Uncomment this to turn on verbose mode.
|
|
||||||
#export DH_VERBOSE=1
|
|
||||||
|
|
||||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@ --with=systemd --with=cli
|
|
||||||
|
|
||||||
# No init script, only systemd
|
|
||||||
override_dh_installinit:
|
|
||||||
true
|
|
||||||
|
|
||||||
# Sonarr likes debug symbols for logging
|
|
||||||
override_dh_clistrip:
|
|
||||||
|
|
||||||
override_dh_makeclilibs:
|
|
||||||
|
|
||||||
override_dh_clideps:
|
|
||||||
dh_clideps -d -r $(patsubst %,--exclude-moduleref=%,$(EXCLUDE_MODULEREFS))
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ignores msbuild
|
|
||||||
ignores libc6
|
|
||||||
@@ -11,7 +11,7 @@ Group=sonarr
|
|||||||
UMask=002
|
UMask=002
|
||||||
|
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/lib/sonarr/bin/Sonarr -nobrowser -data=/var/lib/sonarr
|
ExecStart=/opt/Sonarr/Sonarr -nobrowser -data=/var/lib/sonarr
|
||||||
TimeoutStopSec=20
|
TimeoutStopSec=20
|
||||||
KillMode=process
|
KillMode=process
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
Template: sonarr/owning_user
|
|
||||||
Type: string
|
|
||||||
Default: sonarr
|
|
||||||
Description: Sonarr user:
|
|
||||||
Specify the user that is used to run Sonarr. The user will be created if it does not already exist.
|
|
||||||
The default 'sonarr' should work fine for most users. You can specify the user group next.
|
|
||||||
|
|
||||||
Template: sonarr/owning_group
|
|
||||||
Type: string
|
|
||||||
Default: sonarr
|
|
||||||
Description: Sonarr group:
|
|
||||||
Specify the group that is used to run Sonarr. The group will be created if it does not already exist.
|
|
||||||
If the user doesn't already exist then this group will be used as the user's primary group.
|
|
||||||
Any media files created by Sonarr will be writeable by this group.
|
|
||||||
It's advisable to keep the group the same between download client, Sonarr and media centers.
|
|
||||||
|
|
||||||
Template: sonarr/owning_umask
|
|
||||||
Type: string
|
|
||||||
Default: 0002
|
|
||||||
Description: Sonarr umask:
|
|
||||||
Specifies the umask of the files created by Sonarr. 0002 means the files will be created with 664 as permissions.
|
|
||||||
|
|
||||||
Template: sonarr/config_directory
|
|
||||||
Type: string
|
|
||||||
Default: /var/lib/sonarr
|
|
||||||
Description: Config directory:
|
|
||||||
Specify the directory where Sonarr stores the internal database and metadata. Media content will be stored elsewhere.
|
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.0</string>
|
<string>4.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>xmmd</string>
|
<string>xmmd</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>3.0</string>
|
<string>4.0</string>
|
||||||
<key>NSAppleScriptEnabled</key>
|
<key>NSAppleScriptEnabled</key>
|
||||||
<string>YES</string>
|
<string>YES</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -3,8 +3,5 @@
|
|||||||
@REM SET BRANCH=develop
|
@REM SET BRANCH=develop
|
||||||
@REM SET FRAMEWORK=net6.0
|
@REM SET FRAMEWORK=net6.0
|
||||||
@REM SET RUNTIME=win-x64
|
@REM SET RUNTIME=win-x64
|
||||||
echo ##teamcity[progressStart 'Building setup file']
|
|
||||||
inno\ISCC.exe sonarr.iss
|
|
||||||
echo ##teamcity[progressFinish 'Building setup file']
|
|
||||||
|
|
||||||
echo ##teamcity[publishArtifacts 'distribution\windows\setup\output\*%RUNTIME%*.exe']
|
inno\ISCC.exe sonarr.iss
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ Compression=lzma2/normal
|
|||||||
AppContact={#ForumsURL}
|
AppContact={#ForumsURL}
|
||||||
VersionInfoVersion={#MajorVersion}
|
VersionInfoVersion={#MajorVersion}
|
||||||
SetupLogging=yes
|
SetupLogging=yes
|
||||||
OutputDir=output
|
OutputDir="..\..\..\_artifacts"
|
||||||
AppverName={#AppName}
|
AppverName={#AppName}
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
@@ -53,8 +53,8 @@ Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts
|
|||||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Sonarr\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\..\..\_output\{#Runtime}\{#Framework}\Sonarr\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Sonarr\*"; Excludes: "Sonarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\..\..\_output\{#Runtime}\{#Framework}\Sonarr\*"; Excludes: "Sonarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
@@ -32,7 +33,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
|||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/Sonarr.dll" v3 &
|
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 &
|
||||||
|
|
||||||
sleep 30
|
sleep 30
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const loose = true;
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'@babel/plugin-transform-logical-assignment-operators',
|
||||||
|
|
||||||
// Stage 1
|
// Stage 1
|
||||||
'@babel/plugin-proposal-export-default-from',
|
'@babel/plugin-proposal-export-default-from',
|
||||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType) {
|
function getIconName(eventType, data) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@@ -17,7 +17,7 @@ function getIconName(eventType) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return icons.DELETE;
|
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('DownloadFailedEpisodeTooltip');
|
return translate('DownloadFailedEpisodeTooltip');
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return translate('EpisodeFileDeletedTooltip');
|
return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip');
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return translate('EpisodeFileRenamedTooltip');
|
return translate('EpisodeFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
function HistoryEventTypeCell({ eventType, data }) {
|
||||||
const iconName = getIconName(eventType);
|
const iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
|||||||
import QueueFilterModal from './QueueFilterModal';
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||||
|
|
||||||
class Queue extends Component {
|
class Queue extends Component {
|
||||||
|
|
||||||
@@ -305,9 +305,16 @@ class Queue extends Component {
|
|||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
<RemoveQueueItemsModal
|
<RemoveQueueItemModal
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
selectedCount={selectedCount}
|
selectedCount={selectedCount}
|
||||||
|
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.downloadClientHasPostImportCategory);
|
||||||
|
})
|
||||||
|
)}
|
||||||
canIgnore={isConfirmRemoveModalOpen && (
|
canIgnore={isConfirmRemoveModalOpen && (
|
||||||
selectedIds.every((id) => {
|
selectedIds.every((id) => {
|
||||||
const item = items.find((i) => i.id === id);
|
const item = items.find((i) => i.id === id);
|
||||||
@@ -315,7 +322,7 @@ class Queue extends Component {
|
|||||||
return !!(item && item.seriesId && item.episodeId);
|
return !!(item && item.seriesId && item.episodeId);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
allPending={isConfirmRemoveModalOpen && (
|
pending={isConfirmRemoveModalOpen && (
|
||||||
selectedIds.every((id) => {
|
selectedIds.every((id) => {
|
||||||
const item = items.find((i) => i.id === id);
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ class QueueRow extends Component {
|
|||||||
indexer,
|
indexer,
|
||||||
outputPath,
|
outputPath,
|
||||||
downloadClient,
|
downloadClient,
|
||||||
|
downloadClientHasPostImportCategory,
|
||||||
estimatedCompletionTime,
|
estimatedCompletionTime,
|
||||||
|
added,
|
||||||
timeleft,
|
timeleft,
|
||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
@@ -362,6 +364,15 @@ class QueueRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'added') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={added}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -410,6 +421,7 @@ class QueueRow extends Component {
|
|||||||
<RemoveQueueItemModal
|
<RemoveQueueItemModal
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
sourceTitle={title}
|
sourceTitle={title}
|
||||||
|
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||||
canIgnore={!!series}
|
canIgnore={!!series}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||||
@@ -440,7 +452,9 @@ QueueRow.propTypes = {
|
|||||||
indexer: PropTypes.string,
|
indexer: PropTypes.string,
|
||||||
outputPath: PropTypes.string,
|
outputPath: PropTypes.string,
|
||||||
downloadClient: PropTypes.string,
|
downloadClient: PropTypes.string,
|
||||||
|
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||||
estimatedCompletionTime: PropTypes.string,
|
estimatedCompletionTime: PropTypes.string,
|
||||||
|
added: PropTypes.string,
|
||||||
timeleft: PropTypes.string,
|
timeleft: PropTypes.string,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
sizeleft: PropTypes.number,
|
sizeleft: PropTypes.number,
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class RemoveQueueItemModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveChange = ({ value }) => {
|
|
||||||
this.setState({ remove: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
sourceTitle,
|
|
||||||
canIgnore,
|
|
||||||
isPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { remove, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('RemoveQueueItem', { sourceTitle })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="remove"
|
|
||||||
value={remove}
|
|
||||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
{translate('Remove')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
isPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemModal;
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './RemoveQueueItemModal.css';
|
||||||
|
|
||||||
|
interface RemovePressProps {
|
||||||
|
remove: boolean;
|
||||||
|
changeCategory: boolean;
|
||||||
|
blocklist: boolean;
|
||||||
|
skipRedownload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoveQueueItemModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
sourceTitle: string;
|
||||||
|
canChangeCategory: boolean;
|
||||||
|
canIgnore: boolean;
|
||||||
|
isPending: boolean;
|
||||||
|
selectedCount?: number;
|
||||||
|
onRemovePress(props: RemovePressProps): void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||||
|
type BlocklistMethod =
|
||||||
|
| 'doNotBlocklist'
|
||||||
|
| 'blocklistAndSearch'
|
||||||
|
| 'blocklistOnly';
|
||||||
|
|
||||||
|
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
sourceTitle,
|
||||||
|
canIgnore,
|
||||||
|
canChangeCategory,
|
||||||
|
isPending,
|
||||||
|
selectedCount,
|
||||||
|
onRemovePress,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const multipleSelected = selectedCount && selectedCount > 1;
|
||||||
|
|
||||||
|
const [removalMethod, setRemovalMethod] =
|
||||||
|
useState<RemovalMethod>('removeFromClient');
|
||||||
|
const [blocklistMethod, setBlocklistMethod] =
|
||||||
|
useState<BlocklistMethod>('doNotBlocklist');
|
||||||
|
|
||||||
|
const { title, message } = useMemo(() => {
|
||||||
|
if (!selectedCount) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||||
|
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCount === 1) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItem'),
|
||||||
|
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItems'),
|
||||||
|
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||||
|
selectedCount,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, [sourceTitle, selectedCount]);
|
||||||
|
|
||||||
|
const removalMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'removeFromClient',
|
||||||
|
value: translate('RemoveFromDownloadClient'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('RemoveMultipleFromDownloadClientHint')
|
||||||
|
: translate('RemoveFromDownloadClientHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'changeCategory',
|
||||||
|
value: translate('ChangeCategory'),
|
||||||
|
isDisabled: !canChangeCategory,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('ChangeCategoryMultipleHint')
|
||||||
|
: translate('ChangeCategoryHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ignore',
|
||||||
|
value: multipleSelected
|
||||||
|
? translate('IgnoreDownloads')
|
||||||
|
: translate('IgnoreDownload'),
|
||||||
|
isDisabled: !canIgnore,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('IgnoreDownloadsHint')
|
||||||
|
: translate('IgnoreDownloadHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||||
|
|
||||||
|
const blocklistMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'doNotBlocklist',
|
||||||
|
value: translate('DoNotBlocklist'),
|
||||||
|
hint: translate('DoNotBlocklistHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistAndSearch',
|
||||||
|
value: translate('BlocklistAndSearch'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
|
: translate('BlocklistAndSearchHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistOnly',
|
||||||
|
value: translate('BlocklistOnly'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistMultipleOnlyHint')
|
||||||
|
: translate('BlocklistOnlyHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [multipleSelected]);
|
||||||
|
|
||||||
|
const handleRemovalMethodChange = useCallback(
|
||||||
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
setRemovalMethod(value);
|
||||||
|
},
|
||||||
|
[setRemovalMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlocklistMethodChange = useCallback(
|
||||||
|
({ value }: { value: BlocklistMethod }) => {
|
||||||
|
setBlocklistMethod(value);
|
||||||
|
},
|
||||||
|
[setBlocklistMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmRemove = useCallback(() => {
|
||||||
|
onRemovePress({
|
||||||
|
remove: removalMethod === 'removeFromClient',
|
||||||
|
changeCategory: removalMethod === 'changeCategory',
|
||||||
|
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||||
|
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
}, [
|
||||||
|
removalMethod,
|
||||||
|
blocklistMethod,
|
||||||
|
setRemovalMethod,
|
||||||
|
setBlocklistMethod,
|
||||||
|
onRemovePress,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||||
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
|
<ModalHeader>{title}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.message}>{message}</div>
|
||||||
|
|
||||||
|
{isPending ? null : (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="removalMethod"
|
||||||
|
value={removalMethod}
|
||||||
|
values={removalMethodOptions}
|
||||||
|
isDisabled={!canChangeCategory && !canIgnore}
|
||||||
|
helpTextWarning={translate(
|
||||||
|
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||||
|
)}
|
||||||
|
onChange={handleRemovalMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{multipleSelected
|
||||||
|
? translate('BlocklistReleases')
|
||||||
|
: translate('BlocklistRelease')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="blocklistMethod"
|
||||||
|
value={blocklistMethod}
|
||||||
|
values={blocklistMethodOptions}
|
||||||
|
helpText={translate('BlocklistReleaseHelpText')}
|
||||||
|
onChange={handleBlocklistMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||||
|
{translate('Remove')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoveQueueItemModal;
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './RemoveQueueItemsModal.css';
|
|
||||||
|
|
||||||
class RemoveQueueItemsModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveChange = ({ value }) => {
|
|
||||||
this.setState({ remove: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
selectedCount,
|
|
||||||
canIgnore,
|
|
||||||
allPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { remove, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.message}>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
allPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="remove"
|
|
||||||
value={remove}
|
|
||||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
{translate('Remove')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
selectedCount: PropTypes.number.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
allPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemsModal;
|
|
||||||
@@ -118,7 +118,7 @@ class ImportSeriesSelectFolder extends Component {
|
|||||||
className={styles.addErrorAlert}
|
className={styles.addErrorAlert}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{translate('RootFolderLoadError')}
|
{translate('AddRootFolderError')}
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,16 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
episodeFiles: EpisodeFilesAppState;
|
episodeFiles: EpisodeFilesAppState;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import AppSectionState, {
|
|||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
|
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
@@ -35,12 +37,20 @@ export interface QualityProfilesAppState
|
|||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export interface ImportListOptionsSettingsAppState
|
||||||
|
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
|
advancedSettings: boolean;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
importListOptions: ImportListOptionsSettingsAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
|
|||||||
.statusContainer {
|
.statusContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
filter: var(--calendarFullColorFilter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
|
|||||||
@@ -102,7 +102,12 @@ class CalendarEvent extends Component {
|
|||||||
{series.title}
|
{series.title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.statusContainer}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.statusContainer,
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
|
)}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
missingAbsoluteNumber ?
|
missingAbsoluteNumber ?
|
||||||
<Icon
|
<Icon
|
||||||
@@ -128,6 +133,7 @@ class CalendarEvent extends Component {
|
|||||||
<span className={styles.statusIcon}>
|
<span className={styles.statusIcon}>
|
||||||
<CalendarEventQueueDetails
|
<CalendarEventQueueDetails
|
||||||
{...queueItem}
|
{...queueItem}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
/>
|
/>
|
||||||
</span> :
|
</span> :
|
||||||
null
|
null
|
||||||
@@ -150,7 +156,7 @@ class CalendarEvent extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.EPISODE_FILE}
|
name={icons.EPISODE_FILE}
|
||||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
title={translate('QualityCutoffNotMet')}
|
title={translate('QualityCutoffNotMet')}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
@@ -160,9 +166,8 @@ class CalendarEvent extends Component {
|
|||||||
episodeNumber === 1 && seasonNumber > 0 ?
|
episodeNumber === 1 && seasonNumber > 0 ?
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.INFO}
|
name={icons.PREMIERE}
|
||||||
kind={kinds.INFO}
|
kind={kinds.INFO}
|
||||||
darken={fullColorEvents}
|
|
||||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
@@ -173,8 +178,8 @@ class CalendarEvent extends Component {
|
|||||||
finaleType ?
|
finaleType ?
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.INFO}
|
name={finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON}
|
||||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
kind={finaleType === 'series' ? kinds.DANGER : kinds.WARNING}
|
||||||
title={getFinaleTypeName(finaleType)}
|
title={getFinaleTypeName(finaleType)}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
@@ -187,7 +192,6 @@ class CalendarEvent extends Component {
|
|||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.INFO}
|
name={icons.INFO}
|
||||||
kind={kinds.PINK}
|
kind={kinds.PINK}
|
||||||
darken={fullColorEvents}
|
|
||||||
title={translate('Special')}
|
title={translate('Special')}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
.expandContainer,
|
.expandContainer,
|
||||||
.collapseContainer {
|
.collapseContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +51,15 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
filter: var(--calendarFullColorFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface CssExports {
|
|||||||
'onAir': string;
|
'onAir': string;
|
||||||
'premiere': string;
|
'premiere': string;
|
||||||
'seriesTitle': string;
|
'seriesTitle': string;
|
||||||
|
'statusContainer': string;
|
||||||
'statusIcon': string;
|
'statusIcon': string;
|
||||||
'unaired': string;
|
'unaired': string;
|
||||||
'unmonitored': string;
|
'unmonitored': string;
|
||||||
|
|||||||
@@ -145,45 +145,51 @@ class CalendarEventGroup extends Component {
|
|||||||
{series.title}
|
{series.title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
<div
|
||||||
isMissingAbsoluteNumber &&
|
className={classNames(
|
||||||
<Icon
|
styles.statusContainer,
|
||||||
containerClassName={styles.statusIcon}
|
fullColorEvents && 'fullColor'
|
||||||
name={icons.WARNING}
|
)}
|
||||||
title={translate('EpisodeMissingAbsoluteNumber')}
|
>
|
||||||
/>
|
{
|
||||||
}
|
isMissingAbsoluteNumber &&
|
||||||
|
<Icon
|
||||||
|
containerClassName={styles.statusIcon}
|
||||||
|
name={icons.WARNING}
|
||||||
|
title={translate('EpisodeMissingAbsoluteNumber')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
anyDownloading &&
|
anyDownloading &&
|
||||||
<Icon
|
<Icon
|
||||||
containerClassName={styles.statusIcon}
|
containerClassName={styles.statusIcon}
|
||||||
name={icons.DOWNLOADING}
|
name={icons.DOWNLOADING}
|
||||||
title={translate('AnEpisodeIsDownloading')}
|
title={translate('AnEpisodeIsDownloading')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
|
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
|
||||||
<Icon
|
<Icon
|
||||||
containerClassName={styles.statusIcon}
|
containerClassName={styles.statusIcon}
|
||||||
name={icons.INFO}
|
name={icons.PREMIERE}
|
||||||
kind={kinds.INFO}
|
kind={kinds.INFO}
|
||||||
darken={fullColorEvents}
|
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
showFinaleIcon &&
|
showFinaleIcon &&
|
||||||
lastEpisode.finaleType ?
|
lastEpisode.finaleType ?
|
||||||
<Icon
|
<Icon
|
||||||
containerClassName={styles.statusIcon}
|
containerClassName={styles.statusIcon}
|
||||||
name={icons.INFO}
|
name={lastEpisode.finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON}
|
||||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
kind={lastEpisode.finaleType === 'series' ? kinds.DANGER : kinds.WARNING}
|
||||||
title={getFinaleTypeName(lastEpisode.finaleType)}
|
title={getFinaleTypeName(lastEpisode.finaleType)}
|
||||||
/> : null
|
/> : null
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.airingInfo}>
|
<div className={styles.airingInfo}>
|
||||||
@@ -218,16 +224,19 @@ class CalendarEventGroup extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
showEpisodeInformation &&
|
showEpisodeInformation ?
|
||||||
<Link
|
<Link
|
||||||
className={styles.expandContainer}
|
className={styles.expandContainer}
|
||||||
component="div"
|
component="div"
|
||||||
onPress={this.onExpandPress}
|
onPress={this.onExpandPress}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.EXPAND}
|
name={icons.EXPAND}
|
||||||
/>
|
/>
|
||||||
</Link>
|
|
||||||
|
</Link> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function createIsDownloadingSelector() {
|
|||||||
(state) => state.queue.details,
|
(state) => state.queue.details,
|
||||||
(episodeIds, details) => {
|
(episodeIds, details) => {
|
||||||
return details.items.some((item) => {
|
return details.items.some((item) => {
|
||||||
return item.episode && episodeIds.includes(item.episode.id);
|
return !!(item.episodeId && episodeIds.includes(item.episodeId));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,9 +22,20 @@ function Legend(props) {
|
|||||||
if (showFinaleIcon) {
|
if (showFinaleIcon) {
|
||||||
iconsToShow.push(
|
iconsToShow.push(
|
||||||
<LegendIconItem
|
<LegendIconItem
|
||||||
name="Finale"
|
name={translate('SeasonFinale')}
|
||||||
icon={icons.INFO}
|
icon={icons.FINALE_SEASON}
|
||||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
|
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
iconsToShow.push(
|
||||||
|
<LegendIconItem
|
||||||
|
name={translate('SeriesFinale')}
|
||||||
|
icon={icons.FINALE_SERIES}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -33,10 +44,10 @@ function Legend(props) {
|
|||||||
if (showSpecialIcon) {
|
if (showSpecialIcon) {
|
||||||
iconsToShow.push(
|
iconsToShow.push(
|
||||||
<LegendIconItem
|
<LegendIconItem
|
||||||
name="Special"
|
name={translate('Special')}
|
||||||
icon={icons.INFO}
|
icon={icons.INFO}
|
||||||
kind={kinds.PINK}
|
kind={kinds.PINK}
|
||||||
darken={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
tooltip={translate('SpecialEpisode')}
|
tooltip={translate('SpecialEpisode')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -45,9 +56,10 @@ function Legend(props) {
|
|||||||
if (showCutoffUnmetIcon) {
|
if (showCutoffUnmetIcon) {
|
||||||
iconsToShow.push(
|
iconsToShow.push(
|
||||||
<LegendIconItem
|
<LegendIconItem
|
||||||
name="Cutoff Not Met"
|
name={translate('Cutoff Not Met')}
|
||||||
icon={icons.EPISODE_FILE}
|
icon={icons.EPISODE_FILE}
|
||||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
tooltip={translate('QualityCutoffNotMet')}
|
tooltip={translate('QualityCutoffNotMet')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -112,10 +124,10 @@ function Legend(props) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<LegendIconItem
|
<LegendIconItem
|
||||||
name="Premiere"
|
name={translate('Premiere')}
|
||||||
icon={icons.INFO}
|
icon={icons.PREMIERE}
|
||||||
kind={kinds.INFO}
|
kind={kinds.INFO}
|
||||||
darken={true}
|
fullColorEvents={fullColorEvents}
|
||||||
tooltip={translate('CalendarLegendSeriesPremiereTooltip')}
|
tooltip={translate('CalendarLegendSeriesPremiereTooltip')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -129,6 +141,12 @@ function Legend(props) {
|
|||||||
{iconsToShow[2]}
|
{iconsToShow[2]}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
iconsToShow.length > 3 &&
|
||||||
|
<div>
|
||||||
|
{iconsToShow[3]}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,8 @@
|
|||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
|
&:global(.fullColorEvents) {
|
||||||
|
filter: var(--calendarFullColorFilter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
@@ -6,9 +7,9 @@ import styles from './LegendIconItem.css';
|
|||||||
function LegendIconItem(props) {
|
function LegendIconItem(props) {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
fullColorEvents,
|
||||||
icon,
|
icon,
|
||||||
kind,
|
kind,
|
||||||
darken,
|
|
||||||
tooltip
|
tooltip
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -18,9 +19,11 @@ function LegendIconItem(props) {
|
|||||||
title={tooltip}
|
title={tooltip}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.icon}
|
className={classNames(
|
||||||
|
styles.icon,
|
||||||
|
fullColorEvents && 'fullColorEvents'
|
||||||
|
)}
|
||||||
name={icon}
|
name={icon}
|
||||||
darken={darken}
|
|
||||||
kind={kind}
|
kind={kind}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -31,14 +34,10 @@ function LegendIconItem(props) {
|
|||||||
|
|
||||||
LegendIconItem.propTypes = {
|
LegendIconItem.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
icon: PropTypes.object.isRequired,
|
icon: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
darken: PropTypes.bool.isRequired,
|
|
||||||
tooltip: PropTypes.string.isRequired
|
tooltip: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
LegendIconItem.defaultProps = {
|
|
||||||
darken: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LegendIconItem;
|
export default LegendIconItem;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const CLEAR_LOGS = 'ClearLog';
|
|||||||
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
||||||
export const DELETE_LOG_FILES = 'DeleteLogFiles';
|
export const DELETE_LOG_FILES = 'DeleteLogFiles';
|
||||||
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
|
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
|
||||||
export const DOWNLOADED_EPSIODES_SCAN = 'DownloadedEpisodesScan';
|
export const DOWNLOADED_EPISODES_SCAN = 'DownloadedEpisodesScan';
|
||||||
export const EPISODE_SEARCH = 'EpisodeSearch';
|
export const EPISODE_SEARCH = 'EpisodeSearch';
|
||||||
export const INTERACTIVE_IMPORT = 'ManualImport';
|
export const INTERACTIVE_IMPORT = 'ManualImport';
|
||||||
export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';
|
export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
|
|||||||
className={styles.mappedDrivesWarning}
|
className={styles.mappedDrivesWarning}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService')} />
|
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server' })} />
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,22 +30,24 @@ function CustomFiltersModalContent(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters.map((customFilter) => {
|
customFilters
|
||||||
return (
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
<CustomFilter
|
.map((customFilter) => {
|
||||||
key={customFilter.id}
|
return (
|
||||||
id={customFilter.id}
|
<CustomFilter
|
||||||
label={customFilter.label}
|
key={customFilter.id}
|
||||||
filters={customFilter.filters}
|
id={customFilter.id}
|
||||||
selectedFilterKey={selectedFilterKey}
|
label={customFilter.label}
|
||||||
isDeleting={isDeleting}
|
filters={customFilter.filters}
|
||||||
deleteError={deleteError}
|
selectedFilterKey={selectedFilterKey}
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
isDeleting={isDeleting}
|
||||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
deleteError={deleteError}
|
||||||
onEditPress={onEditCustomFilter}
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
/>
|
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||||
);
|
onEditPress={onEditCustomFilter}
|
||||||
})
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.addButtonContainer}>
|
<div className={styles.addButtonContainer}>
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ function createMapStateToProps() {
|
|||||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||||
return {
|
return {
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name
|
value: downloadClient.name,
|
||||||
|
hint: `(${downloadClient.id})`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
|||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
|
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
||||||
@@ -71,6 +72,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.INDEXER_SELECT:
|
case inputTypes.INDEXER_SELECT:
|
||||||
return IndexerSelectInputConnector;
|
return IndexerSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
|
return IndexerFlagsSelectInput;
|
||||||
|
|
||||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||||
return DownloadClientSelectInputConnector;
|
return DownloadClientSelectInputConnector;
|
||||||
|
|
||||||
@@ -264,6 +268,7 @@ FormInputGroup.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
@@ -278,6 +283,7 @@ FormInputGroup.propTypes = {
|
|||||||
includeNoChange: PropTypes.bool,
|
includeNoChange: PropTypes.bool,
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
selectedValueOptions: PropTypes.object,
|
selectedValueOptions: PropTypes.object,
|
||||||
|
indexerFlags: PropTypes.number,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||||
|
createSelector(
|
||||||
|
(state: AppState) => state.settings.indexerFlags,
|
||||||
|
(indexerFlags) => {
|
||||||
|
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
if ((selectedFlags & id) === id) {
|
||||||
|
acc.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const values = indexerFlags.items.map(({ id, name }) => ({
|
||||||
|
key: id,
|
||||||
|
value: name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IndexerFlagsSelectInputProps {
|
||||||
|
name: string;
|
||||||
|
indexerFlags: number;
|
||||||
|
onChange(payload: object): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
||||||
|
const { indexerFlags, onChange } = props;
|
||||||
|
|
||||||
|
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
||||||
|
|
||||||
|
const onChangeWrapper = useCallback(
|
||||||
|
({ name, value }: { name: string; value: number[] }) => {
|
||||||
|
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
||||||
|
|
||||||
|
onChange({ name, value: indexerFlags });
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
values={values}
|
||||||
|
onChange={onChangeWrapper}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlagsSelectInput;
|
||||||
@@ -91,6 +91,7 @@ class TextTagInputConnector extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<TagInput
|
<TagInput
|
||||||
|
delimiters={['Tab', 'Enter', ',']}
|
||||||
tagList={[]}
|
tagList={[]}
|
||||||
onTagAdd={this.onTagAdd}
|
onTagAdd={this.onTagAdd}
|
||||||
onTagDelete={this.onTagDelete}
|
onTagDelete={this.onTagDelete}
|
||||||
|
|||||||
@@ -12,18 +12,10 @@
|
|||||||
|
|
||||||
.info {
|
.info {
|
||||||
color: var(--infoColor);
|
color: var(--infoColor);
|
||||||
|
|
||||||
&:global(.darken) {
|
|
||||||
color: color(var(--infoColor) shade(30%));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pink {
|
.pink {
|
||||||
color: var(--pink);
|
color: var(--pink);
|
||||||
|
|
||||||
&:global(.darken) {
|
|
||||||
color: color(var(--pink) shade(30%));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class Icon extends PureComponent {
|
|||||||
kind,
|
kind,
|
||||||
size,
|
size,
|
||||||
title,
|
title,
|
||||||
darken,
|
|
||||||
isSpinning,
|
isSpinning,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -27,8 +26,7 @@ class Icon extends PureComponent {
|
|||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
styles[kind],
|
styles[kind]
|
||||||
darken && 'darken'
|
|
||||||
)}
|
)}
|
||||||
icon={name}
|
icon={name}
|
||||||
spin={isSpinning}
|
spin={isSpinning}
|
||||||
@@ -61,7 +59,6 @@ Icon.propTypes = {
|
|||||||
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.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
darken: PropTypes.bool.isRequired,
|
|
||||||
isSpinning: PropTypes.bool.isRequired,
|
isSpinning: PropTypes.bool.isRequired,
|
||||||
fixedWidth: PropTypes.bool.isRequired
|
fixedWidth: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
@@ -69,7 +66,6 @@ Icon.propTypes = {
|
|||||||
Icon.defaultProps = {
|
Icon.defaultProps = {
|
||||||
kind: kinds.DEFAULT,
|
kind: kinds.DEFAULT,
|
||||||
size: 14,
|
size: 14,
|
||||||
darken: false,
|
|
||||||
isSpinning: false,
|
isSpinning: false,
|
||||||
fixedWidth: false
|
fixedWidth: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,18 +40,26 @@ class FilterMenuContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
customFilters.map((filter) => {
|
customFilters.length > 0 ?
|
||||||
return (
|
<MenuItemSeparator /> :
|
||||||
<FilterMenuItem
|
null
|
||||||
key={filter.id}
|
}
|
||||||
filterKey={filter.id}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
{
|
||||||
onPress={onFilterSelect}
|
customFilters
|
||||||
>
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
{filter.label}
|
.map((filter) => {
|
||||||
</FilterMenuItem>
|
return (
|
||||||
);
|
<FilterMenuItem
|
||||||
})
|
key={filter.id}
|
||||||
|
filterKey={filter.id}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
onPress={onFilterSelect}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</FilterMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import { createSelector } from 'reselect';
|
|||||||
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchSeries } from 'Store/Actions/seriesActions';
|
import { fetchSeries } from 'Store/Actions/seriesActions';
|
||||||
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
fetchImportLists,
|
||||||
|
fetchIndexerFlags,
|
||||||
|
fetchLanguages,
|
||||||
|
fetchQualityProfiles,
|
||||||
|
fetchUISettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
@@ -51,6 +57,7 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.settings.qualityProfiles.isPopulated,
|
(state) => state.settings.qualityProfiles.isPopulated,
|
||||||
(state) => state.settings.languages.isPopulated,
|
(state) => state.settings.languages.isPopulated,
|
||||||
(state) => state.settings.importLists.isPopulated,
|
(state) => state.settings.importLists.isPopulated,
|
||||||
|
(state) => state.settings.indexerFlags.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
(state) => state.app.translations.isPopulated,
|
(state) => state.app.translations.isPopulated,
|
||||||
(
|
(
|
||||||
@@ -61,6 +68,7 @@ const selectIsPopulated = createSelector(
|
|||||||
qualityProfilesIsPopulated,
|
qualityProfilesIsPopulated,
|
||||||
languagesIsPopulated,
|
languagesIsPopulated,
|
||||||
importListsIsPopulated,
|
importListsIsPopulated,
|
||||||
|
indexerFlagsIsPopulated,
|
||||||
systemStatusIsPopulated,
|
systemStatusIsPopulated,
|
||||||
translationsIsPopulated
|
translationsIsPopulated
|
||||||
) => {
|
) => {
|
||||||
@@ -72,6 +80,7 @@ const selectIsPopulated = createSelector(
|
|||||||
qualityProfilesIsPopulated &&
|
qualityProfilesIsPopulated &&
|
||||||
languagesIsPopulated &&
|
languagesIsPopulated &&
|
||||||
importListsIsPopulated &&
|
importListsIsPopulated &&
|
||||||
|
indexerFlagsIsPopulated &&
|
||||||
systemStatusIsPopulated &&
|
systemStatusIsPopulated &&
|
||||||
translationsIsPopulated
|
translationsIsPopulated
|
||||||
);
|
);
|
||||||
@@ -86,6 +95,7 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.settings.qualityProfiles.error,
|
(state) => state.settings.qualityProfiles.error,
|
||||||
(state) => state.settings.languages.error,
|
(state) => state.settings.languages.error,
|
||||||
(state) => state.settings.importLists.error,
|
(state) => state.settings.importLists.error,
|
||||||
|
(state) => state.settings.indexerFlags.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
(state) => state.app.translations.error,
|
(state) => state.app.translations.error,
|
||||||
(
|
(
|
||||||
@@ -96,6 +106,7 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
languagesError,
|
languagesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
|
indexerFlagsError,
|
||||||
systemStatusError,
|
systemStatusError,
|
||||||
translationsError
|
translationsError
|
||||||
) => {
|
) => {
|
||||||
@@ -107,6 +118,7 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError ||
|
qualityProfilesError ||
|
||||||
languagesError ||
|
languagesError ||
|
||||||
importListsError ||
|
importListsError ||
|
||||||
|
indexerFlagsError ||
|
||||||
systemStatusError ||
|
systemStatusError ||
|
||||||
translationsError
|
translationsError
|
||||||
);
|
);
|
||||||
@@ -120,6 +132,7 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
languagesError,
|
languagesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
|
indexerFlagsError,
|
||||||
systemStatusError,
|
systemStatusError,
|
||||||
translationsError
|
translationsError
|
||||||
};
|
};
|
||||||
@@ -174,6 +187,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchImportLists() {
|
dispatchFetchImportLists() {
|
||||||
dispatch(fetchImportLists());
|
dispatch(fetchImportLists());
|
||||||
},
|
},
|
||||||
|
dispatchFetchIndexerFlags() {
|
||||||
|
dispatch(fetchIndexerFlags());
|
||||||
|
},
|
||||||
dispatchFetchUISettings() {
|
dispatchFetchUISettings() {
|
||||||
dispatch(fetchUISettings());
|
dispatch(fetchUISettings());
|
||||||
},
|
},
|
||||||
@@ -213,6 +229,7 @@ class PageConnector extends Component {
|
|||||||
this.props.dispatchFetchQualityProfiles();
|
this.props.dispatchFetchQualityProfiles();
|
||||||
this.props.dispatchFetchLanguages();
|
this.props.dispatchFetchLanguages();
|
||||||
this.props.dispatchFetchImportLists();
|
this.props.dispatchFetchImportLists();
|
||||||
|
this.props.dispatchFetchIndexerFlags();
|
||||||
this.props.dispatchFetchUISettings();
|
this.props.dispatchFetchUISettings();
|
||||||
this.props.dispatchFetchStatus();
|
this.props.dispatchFetchStatus();
|
||||||
this.props.dispatchFetchTranslations();
|
this.props.dispatchFetchTranslations();
|
||||||
@@ -238,6 +255,7 @@ class PageConnector extends Component {
|
|||||||
dispatchFetchQualityProfiles,
|
dispatchFetchQualityProfiles,
|
||||||
dispatchFetchLanguages,
|
dispatchFetchLanguages,
|
||||||
dispatchFetchImportLists,
|
dispatchFetchImportLists,
|
||||||
|
dispatchFetchIndexerFlags,
|
||||||
dispatchFetchUISettings,
|
dispatchFetchUISettings,
|
||||||
dispatchFetchStatus,
|
dispatchFetchStatus,
|
||||||
dispatchFetchTranslations,
|
dispatchFetchTranslations,
|
||||||
@@ -278,6 +296,7 @@ PageConnector.propTypes = {
|
|||||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
||||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||||
dispatchFetchTranslations: PropTypes.func.isRequired,
|
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function getTooltip(title, quality, size) {
|
function getTooltip(title, quality, size) {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
@@ -26,13 +27,44 @@ function getTooltip(title, quality, size) {
|
|||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function revisionLabel(className, quality, showRevision) {
|
||||||
|
if (!showRevision) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.isRepack) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Repack')}
|
||||||
|
>
|
||||||
|
R
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.version && quality.revision.version > 1) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Proper')}
|
||||||
|
>
|
||||||
|
P
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function EpisodeQuality(props) {
|
function EpisodeQuality(props) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
quality,
|
quality,
|
||||||
size,
|
size,
|
||||||
isCutoffNotMet
|
isCutoffNotMet,
|
||||||
|
showRevision
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (!quality) {
|
if (!quality) {
|
||||||
@@ -40,13 +72,15 @@ function EpisodeQuality(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<span>
|
||||||
className={className}
|
<Label
|
||||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
className={className}
|
||||||
title={getTooltip(title, quality, size)}
|
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||||
>
|
title={getTooltip(title, quality, size)}
|
||||||
{quality.quality.name}
|
>
|
||||||
</Label>
|
{quality.quality.name}
|
||||||
|
</Label>{revisionLabel(className, quality, showRevision)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +89,13 @@ EpisodeQuality.propTypes = {
|
|||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
isCutoffNotMet: PropTypes.bool
|
isCutoffNotMet: PropTypes.bool,
|
||||||
|
showRevision: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeQuality.defaultProps = {
|
EpisodeQuality.defaultProps = {
|
||||||
title: ''
|
title: '',
|
||||||
|
showRevision: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EpisodeQuality;
|
export default EpisodeQuality;
|
||||||
|
|||||||
@@ -30,13 +30,9 @@ const columns = [
|
|||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'customFormats',
|
||||||
label: () => translate('Date'),
|
label: () => translate('CustomFormats'),
|
||||||
isVisible: true
|
isSortable: false,
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'details',
|
|
||||||
label: () => translate('Details'),
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,9 +44,13 @@ const columns = [
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
label: () => translate('Date'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: () => translate('Actions'),
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
.details,
|
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'details': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
@@ -102,11 +101,19 @@ class EpisodeHistoryRow extends Component {
|
|||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
date={date}
|
date={date}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.details}>
|
<TableRowCell className={styles.actions}>
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
@@ -124,24 +131,13 @@ class EpisodeHistoryRow extends Component {
|
|||||||
}
|
}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
|
||||||
<Tooltip
|
|
||||||
anchor={
|
|
||||||
formatCustomFormatScore(customFormatScore, customFormats.length)
|
|
||||||
}
|
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
{
|
{
|
||||||
eventType === 'grabbed' &&
|
eventType === 'grabbed' &&
|
||||||
<IconButton
|
<IconButton
|
||||||
title={translate('MarkAsFailed')}
|
title={translate('MarkAsFailed')}
|
||||||
name={icons.REMOVE}
|
name={icons.REMOVE}
|
||||||
|
size={14}
|
||||||
onPress={this.onMarkAsFailedPress}
|
onPress={this.onMarkAsFailedPress}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
||||||
|
|
||||||
|
interface IndexerFlagsProps {
|
||||||
|
indexerFlags: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
||||||
|
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
||||||
|
|
||||||
|
const flags = allIndexerFlags.items.filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(item) => (indexerFlags & item.id) === item.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return flags.length ? (
|
||||||
|
<ul>
|
||||||
|
{flags.map((flag, index) => {
|
||||||
|
return <li key={index}>{flag.name}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlags;
|
||||||
@@ -16,6 +16,7 @@ export interface EpisodeFile extends ModelBase {
|
|||||||
languages: Language[];
|
languages: Language[];
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
customFormats: CustomFormat[];
|
customFormats: CustomFormat[];
|
||||||
|
indexerFlags: number;
|
||||||
mediaInfo: MediaInfo;
|
mediaInfo: MediaInfo;
|
||||||
qualityCutoffNotMet: boolean;
|
qualityCutoffNotMet: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ import {
|
|||||||
faChevronCircleUp as fasChevronCircleUp,
|
faChevronCircleUp as fasChevronCircleUp,
|
||||||
faCircle as fasCircle,
|
faCircle as fasCircle,
|
||||||
faCircleDown as fasCircleDown,
|
faCircleDown as fasCircleDown,
|
||||||
|
faCirclePause as fasCirclePause,
|
||||||
|
faCirclePlay as fasCirclePlay,
|
||||||
|
faCircleStop as fasCircleStop,
|
||||||
faCloud as fasCloud,
|
faCloud as fasCloud,
|
||||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||||
faCog as fasCog,
|
faCog as fasCog,
|
||||||
@@ -55,9 +58,11 @@ import {
|
|||||||
faEye as fasEye,
|
faEye as fasEye,
|
||||||
faFastBackward as fasFastBackward,
|
faFastBackward as fasFastBackward,
|
||||||
faFastForward as fasFastForward,
|
faFastForward as fasFastForward,
|
||||||
|
faFileCircleQuestion as fasFileCircleQuestion,
|
||||||
faFileExport as fasFileExport,
|
faFileExport as fasFileExport,
|
||||||
faFileInvoice as farFileInvoice,
|
faFileInvoice as farFileInvoice,
|
||||||
faFilter as fasFilter,
|
faFilter as fasFilter,
|
||||||
|
faFlag as fasFlag,
|
||||||
faFolderOpen as fasFolderOpen,
|
faFolderOpen as fasFolderOpen,
|
||||||
faForward as fasForward,
|
faForward as fasForward,
|
||||||
faHeart as fasHeart,
|
faHeart as fasHeart,
|
||||||
@@ -146,7 +151,11 @@ export const EXPORT = fasFileExport;
|
|||||||
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
||||||
export const FATAL = fasTimesCircle;
|
export const FATAL = fasTimesCircle;
|
||||||
export const FILE = farFile;
|
export const FILE = farFile;
|
||||||
|
export const FILE_MISSING = fasFileCircleQuestion;
|
||||||
export const FILTER = fasFilter;
|
export const FILTER = fasFilter;
|
||||||
|
export const FINALE_SEASON = fasCirclePause;
|
||||||
|
export const FINALE_SERIES = fasCircleStop;
|
||||||
|
export const FLAG = fasFlag;
|
||||||
export const FOOTNOTE = fasAsterisk;
|
export const FOOTNOTE = fasAsterisk;
|
||||||
export const FOLDER = farFolder;
|
export const FOLDER = farFolder;
|
||||||
export const FOLDER_OPEN = fasFolderOpen;
|
export const FOLDER_OPEN = fasFolderOpen;
|
||||||
@@ -178,6 +187,7 @@ export const PARENT = fasLevelUpAlt;
|
|||||||
export const PARSE = fasCalculator;
|
export const PARSE = fasCalculator;
|
||||||
export const PAUSED = fasPause;
|
export const PAUSED = fasPause;
|
||||||
export const PENDING = farClock;
|
export const PENDING = farClock;
|
||||||
|
export const PREMIERE = fasCirclePlay;
|
||||||
export const PROFILE = fasUser;
|
export const PROFILE = fasUser;
|
||||||
export const POSTER = fasTh;
|
export const POSTER = fasTh;
|
||||||
export const QUEUED = fasCloud;
|
export const QUEUED = fasCloud;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const PASSWORD = 'password';
|
|||||||
export const PATH = 'path';
|
export const PATH = 'path';
|
||||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||||
export const INDEXER_SELECT = 'indexerSelect';
|
export const INDEXER_SELECT = 'indexerSelect';
|
||||||
|
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||||
export const LANGUAGE_SELECT = 'languageSelect';
|
export const LANGUAGE_SELECT = 'languageSelect';
|
||||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function InteractiveImportSelectFolderModalContent(
|
|||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
executeCommand({
|
executeCommand({
|
||||||
name: commandNames.DOWNLOADED_EPSIODES_SCAN,
|
name: commandNames.DOWNLOADED_EPISODES_SCAN,
|
||||||
path: folder,
|
path: folder,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
|
||||||
|
|
||||||
|
interface SelectIndexerFlagsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
indexerFlags: number;
|
||||||
|
modalTitle: string;
|
||||||
|
onIndexerFlagsSelect(indexerFlags: number): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectIndexerFlagsModal(props: SelectIndexerFlagsModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
indexerFlags,
|
||||||
|
modalTitle,
|
||||||
|
onIndexerFlagsSelect,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<SelectIndexerFlagsModalContent
|
||||||
|
indexerFlags={indexerFlags}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectIndexerFlagsModal;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.modalBody {
|
||||||
|
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'modalBody': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SelectIndexerFlagsModalContent.css';
|
||||||
|
|
||||||
|
interface SelectIndexerFlagsModalContentProps {
|
||||||
|
indexerFlags: number;
|
||||||
|
modalTitle: string;
|
||||||
|
onIndexerFlagsSelect(indexerFlags: number): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectIndexerFlagsModalContent(
|
||||||
|
props: SelectIndexerFlagsModalContentProps
|
||||||
|
) {
|
||||||
|
const { modalTitle, onIndexerFlagsSelect, onModalClose } = props;
|
||||||
|
const [indexerFlags, setIndexerFlags] = useState(props.indexerFlags);
|
||||||
|
|
||||||
|
const onIndexerFlagsChange = useCallback(
|
||||||
|
({ value }: { value: number }) => {
|
||||||
|
setIndexerFlags(value);
|
||||||
|
},
|
||||||
|
[setIndexerFlags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onIndexerFlagsSelectWrapper = useCallback(() => {
|
||||||
|
onIndexerFlagsSelect(indexerFlags);
|
||||||
|
}, [indexerFlags, onIndexerFlagsSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('SetIndexerFlagsModalTitle', { modalTitle })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody
|
||||||
|
className={styles.modalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IndexerFlags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.INDEXER_FLAGS_SELECT}
|
||||||
|
name="indexerFlags"
|
||||||
|
indexerFlags={indexerFlags}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={onIndexerFlagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.SUCCESS} onPress={onIndexerFlagsSelectWrapper}>
|
||||||
|
{translate('SetIndexerFlags')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectIndexerFlagsModalContent;
|
||||||
@@ -29,6 +29,7 @@ import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
|||||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||||
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
||||||
import ImportMode from 'InteractiveImport/ImportMode';
|
import ImportMode from 'InteractiveImport/ImportMode';
|
||||||
|
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||||
import InteractiveImport, {
|
import InteractiveImport, {
|
||||||
InteractiveImportCommandOptions,
|
InteractiveImportCommandOptions,
|
||||||
} from 'InteractiveImport/InteractiveImport';
|
} from 'InteractiveImport/InteractiveImport';
|
||||||
@@ -71,7 +72,8 @@ type SelectType =
|
|||||||
| 'episode'
|
| 'episode'
|
||||||
| 'releaseGroup'
|
| 'releaseGroup'
|
||||||
| 'quality'
|
| 'quality'
|
||||||
| 'language';
|
| 'language'
|
||||||
|
| 'indexerFlags';
|
||||||
|
|
||||||
type FilterExistingFiles = 'all' | 'new';
|
type FilterExistingFiles = 'all' | 'new';
|
||||||
|
|
||||||
@@ -135,11 +137,21 @@ const COLUMNS = [
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'indexerFlags',
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.FLAG,
|
||||||
|
title: () => translate('IndexerFlags'),
|
||||||
|
}),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'rejections',
|
name: 'rejections',
|
||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
name: icons.DANGER,
|
name: icons.DANGER,
|
||||||
kind: kinds.DANGER,
|
kind: kinds.DANGER,
|
||||||
|
title: () => translate('Rejections'),
|
||||||
}),
|
}),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
@@ -269,33 +281,6 @@ function InteractiveImportModalContent(
|
|||||||
const [interactiveImportErrorMessage, setInteractiveImportErrorMessage] =
|
const [interactiveImportErrorMessage, setInteractiveImportErrorMessage] =
|
||||||
useState<string | null>(null);
|
useState<string | null>(null);
|
||||||
const [selectState, setSelectState] = useSelectState();
|
const [selectState, setSelectState] = useSelectState();
|
||||||
const [bulkSelectOptions, setBulkSelectOptions] = useState([
|
|
||||||
{
|
|
||||||
key: 'select',
|
|
||||||
value: translate('SelectDropdown'),
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'season',
|
|
||||||
value: translate('SelectSeason'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'episode',
|
|
||||||
value: translate('SelectEpisodes'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'quality',
|
|
||||||
value: translate('SelectQuality'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'releaseGroup',
|
|
||||||
value: translate('SelectReleaseGroup'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'language',
|
|
||||||
value: translate('SelectLanguage'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const { allSelected, allUnselected, selectedState } = selectState;
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
const previousIsDeleting = usePrevious(isDeleting);
|
const previousIsDeleting = usePrevious(isDeleting);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -311,26 +296,93 @@ function InteractiveImportModalContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showIndexerFlags = items.some((item) => item.indexerFlags);
|
||||||
|
|
||||||
|
if (!showIndexerFlags) {
|
||||||
|
const indexerFlagsColumn = result.find((c) => c.name === 'indexerFlags');
|
||||||
|
|
||||||
|
if (indexerFlagsColumn) {
|
||||||
|
indexerFlagsColumn.isVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [showSeries]);
|
}, [showSeries, items]);
|
||||||
|
|
||||||
const selectedIds: number[] = useMemo(() => {
|
const selectedIds: number[] = useMemo(() => {
|
||||||
return getSelectedIds(selectedState);
|
return getSelectedIds(selectedState);
|
||||||
}, [selectedState]);
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const bulkSelectOptions = useMemo(() => {
|
||||||
|
const { seasonSelectDisabled, episodeSelectDisabled } = items.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
if (!selectedIds.includes(item.id)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSelectedSeason = acc.lastSelectedSeason;
|
||||||
|
|
||||||
|
acc.seasonSelectDisabled ||= !item.series;
|
||||||
|
acc.episodeSelectDisabled ||=
|
||||||
|
item.seasonNumber === undefined ||
|
||||||
|
(lastSelectedSeason >= 0 && item.seasonNumber !== lastSelectedSeason);
|
||||||
|
acc.lastSelectedSeason = item.seasonNumber ?? -1;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seasonSelectDisabled: false,
|
||||||
|
episodeSelectDisabled: false,
|
||||||
|
lastSelectedSeason: -1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
key: 'select',
|
||||||
|
value: translate('SelectDropdown'),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'season',
|
||||||
|
value: translate('SelectSeason'),
|
||||||
|
disabled: seasonSelectDisabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'episode',
|
||||||
|
value: translate('SelectEpisodes'),
|
||||||
|
disabled: episodeSelectDisabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'quality',
|
||||||
|
value: translate('SelectQuality'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'releaseGroup',
|
||||||
|
value: translate('SelectReleaseGroup'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'language',
|
||||||
|
value: translate('SelectLanguage'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'indexerFlags',
|
||||||
|
value: translate('SelectIndexerFlags'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowSeriesChange) {
|
||||||
|
options.splice(1, 0, {
|
||||||
|
key: 'series',
|
||||||
|
value: translate('SelectSeries'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [allowSeriesChange, items, selectedIds]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (allowSeriesChange) {
|
|
||||||
const newBulkSelectOptions = [...bulkSelectOptions];
|
|
||||||
|
|
||||||
newBulkSelectOptions.splice(1, 0, {
|
|
||||||
key: 'series',
|
|
||||||
value: translate('SelectSeries'),
|
|
||||||
});
|
|
||||||
|
|
||||||
setBulkSelectOptions(newBulkSelectOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialSortKey) {
|
if (initialSortKey) {
|
||||||
const sortProps: { sortKey: string; sortDirection?: string } = {
|
const sortProps: { sortKey: string; sortDirection?: string } = {
|
||||||
sortKey: initialSortKey,
|
sortKey: initialSortKey,
|
||||||
@@ -457,6 +509,7 @@ function InteractiveImportModalContent(
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
quality,
|
quality,
|
||||||
languages,
|
languages,
|
||||||
|
indexerFlags,
|
||||||
episodeFileId,
|
episodeFileId,
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
@@ -506,6 +559,7 @@ function InteractiveImportModalContent(
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
quality,
|
quality,
|
||||||
languages,
|
languages,
|
||||||
|
indexerFlags,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -520,6 +574,7 @@ function InteractiveImportModalContent(
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
quality,
|
quality,
|
||||||
languages,
|
languages,
|
||||||
|
indexerFlags,
|
||||||
downloadId,
|
downloadId,
|
||||||
episodeFileId,
|
episodeFileId,
|
||||||
});
|
});
|
||||||
@@ -716,6 +771,22 @@ function InteractiveImportModalContent(
|
|||||||
[selectedIds, dispatch]
|
[selectedIds, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onIndexerFlagsSelect = useCallback(
|
||||||
|
(indexerFlags: number) => {
|
||||||
|
dispatch(
|
||||||
|
updateInteractiveImportItems({
|
||||||
|
ids: selectedIds,
|
||||||
|
indexerFlags,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||||
|
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const orderedSelectedIds = items.reduce((acc: number[], file) => {
|
const orderedSelectedIds = items.reduce((acc: number[], file) => {
|
||||||
if (selectedIds.includes(file.id)) {
|
if (selectedIds.includes(file.id)) {
|
||||||
acc.push(file.id);
|
acc.push(file.id);
|
||||||
@@ -921,6 +992,14 @@ function InteractiveImportModalContent(
|
|||||||
onModalClose={onSelectModalClose}
|
onModalClose={onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectIndexerFlagsModal
|
||||||
|
isOpen={selectModalOpen === 'indexerFlags'}
|
||||||
|
indexerFlags={0}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isConfirmDeleteModalOpen}
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import Episode from 'Episode/Episode';
|
|||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import IndexerFlags from 'Episode/IndexerFlags';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||||
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
||||||
|
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||||
@@ -41,7 +43,8 @@ type SelectType =
|
|||||||
| 'episode'
|
| 'episode'
|
||||||
| 'releaseGroup'
|
| 'releaseGroup'
|
||||||
| 'quality'
|
| 'quality'
|
||||||
| 'language';
|
| 'language'
|
||||||
|
| 'indexerFlags';
|
||||||
|
|
||||||
type SelectedChangeProps = SelectStateInputProps & {
|
type SelectedChangeProps = SelectStateInputProps & {
|
||||||
hasEpisodeFileId: boolean;
|
hasEpisodeFileId: boolean;
|
||||||
@@ -60,6 +63,7 @@ interface InteractiveImportRowProps {
|
|||||||
size: number;
|
size: number;
|
||||||
customFormats?: object[];
|
customFormats?: object[];
|
||||||
customFormatScore?: number;
|
customFormatScore?: number;
|
||||||
|
indexerFlags: number;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
episodeFileId?: number;
|
episodeFileId?: number;
|
||||||
@@ -84,6 +88,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
size,
|
size,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
|
indexerFlags,
|
||||||
rejections,
|
rejections,
|
||||||
isReprocessing,
|
isReprocessing,
|
||||||
isSelected,
|
isSelected,
|
||||||
@@ -100,6 +105,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
|
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
|
||||||
[columns]
|
[columns]
|
||||||
);
|
);
|
||||||
|
const isIndexerFlagsColumnVisible = useMemo(
|
||||||
|
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
|
||||||
|
[columns]
|
||||||
|
);
|
||||||
|
|
||||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||||
null
|
null
|
||||||
@@ -306,6 +315,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSelectIndexerFlagsPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('indexerFlags');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onIndexerFlagsSelect = useCallback(
|
||||||
|
(indexerFlags: number) => {
|
||||||
|
dispatch(
|
||||||
|
updateInteractiveImportItem({
|
||||||
|
id,
|
||||||
|
indexerFlags,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
|
||||||
|
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
selectRowAfterChange();
|
||||||
|
},
|
||||||
|
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||||
|
);
|
||||||
|
|
||||||
const seriesTitle = series ? series.title : '';
|
const seriesTitle = series ? series.title : '';
|
||||||
const isAnime = series?.seriesType === 'anime';
|
const isAnime = series?.seriesType === 'anime';
|
||||||
|
|
||||||
@@ -332,6 +362,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
||||||
const showQualityPlaceholder = isSelected && !quality;
|
const showQualityPlaceholder = isSelected && !quality;
|
||||||
const showLanguagePlaceholder = isSelected && !languages;
|
const showLanguagePlaceholder = isSelected && !languages;
|
||||||
|
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -448,6 +479,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
{isIndexerFlagsColumnVisible ? (
|
||||||
|
<TableRowCellButton
|
||||||
|
title={translate('ClickToChangeIndexerFlags')}
|
||||||
|
onPress={onSelectIndexerFlagsPress}
|
||||||
|
>
|
||||||
|
{showIndexerFlagsPlaceholder ? (
|
||||||
|
<InteractiveImportRowCellPlaceholder isOptional={true} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{indexerFlags ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableRowCellButton>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
{rejections.length ? (
|
{rejections.length ? (
|
||||||
<Popover
|
<Popover
|
||||||
@@ -518,6 +571,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
onLanguagesSelect={onLanguagesSelect}
|
onLanguagesSelect={onLanguagesSelect}
|
||||||
onModalClose={onSelectModalClose}
|
onModalClose={onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectIndexerFlagsModal
|
||||||
|
isOpen={selectModalOpen === 'indexerFlags'}
|
||||||
|
indexerFlags={indexerFlags ?? 0}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface InteractiveImportCommandOptions {
|
|||||||
releaseGroup?: string;
|
releaseGroup?: string;
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
languages: Language[];
|
languages: Language[];
|
||||||
|
indexerFlags: number;
|
||||||
downloadId?: string;
|
downloadId?: string;
|
||||||
episodeFileId?: number;
|
episodeFileId?: number;
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,7 @@ interface InteractiveImport extends ModelBase {
|
|||||||
episodes: Episode[];
|
episodes: Episode[];
|
||||||
qualityWeight: number;
|
qualityWeight: number;
|
||||||
customFormats: object[];
|
customFormats: object[];
|
||||||
|
indexerFlags: number;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
episodeFileId?: number;
|
episodeFileId?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,15 @@ const columns = [
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'indexerFlags',
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.FLAG,
|
||||||
|
title: () => translate('IndexerFlags')
|
||||||
|
}),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'rejections',
|
name: 'rejections',
|
||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.languages {
|
.languages {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
@@ -40,7 +44,8 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rejected {
|
.rejected,
|
||||||
|
.indexerFlags {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface CssExports {
|
|||||||
'download': string;
|
'download': string;
|
||||||
'downloadIcon': string;
|
'downloadIcon': string;
|
||||||
'indexer': string;
|
'indexer': string;
|
||||||
|
'indexerFlags': string;
|
||||||
'interactiveIcon': string;
|
'interactiveIcon': string;
|
||||||
'languages': string;
|
'languages': string;
|
||||||
'manualDownloadContent': string;
|
'manualDownloadContent': string;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import IndexerFlags from 'Episode/IndexerFlags';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import { QualityModel } from 'Quality/Quality';
|
import { QualityModel } from 'Quality/Quality';
|
||||||
@@ -98,6 +99,7 @@ interface InteractiveSearchRowProps {
|
|||||||
mappedEpisodeNumbers?: number[];
|
mappedEpisodeNumbers?: number[];
|
||||||
mappedAbsoluteEpisodeNumbers?: number[];
|
mappedAbsoluteEpisodeNumbers?: number[];
|
||||||
mappedEpisodeInfo: ReleaseEpisode[];
|
mappedEpisodeInfo: ReleaseEpisode[];
|
||||||
|
indexerFlags: number;
|
||||||
rejections: string[];
|
rejections: string[];
|
||||||
episodeRequested: boolean;
|
episodeRequested: boolean;
|
||||||
downloadAllowed: boolean;
|
downloadAllowed: boolean;
|
||||||
@@ -139,6 +141,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||||||
mappedEpisodeNumbers,
|
mappedEpisodeNumbers,
|
||||||
mappedAbsoluteEpisodeNumbers,
|
mappedAbsoluteEpisodeNumbers,
|
||||||
mappedEpisodeInfo,
|
mappedEpisodeInfo,
|
||||||
|
indexerFlags = 0,
|
||||||
rejections = [],
|
rejections = [],
|
||||||
episodeRequested,
|
episodeRequested,
|
||||||
downloadAllowed,
|
downloadAllowed,
|
||||||
@@ -244,7 +247,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.quality}>
|
<TableRowCell className={styles.quality}>
|
||||||
<EpisodeQuality quality={quality} />
|
<EpisodeQuality quality={quality} showRevision={true} />
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
@@ -254,10 +257,21 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||||||
customFormats.length
|
customFormats.length
|
||||||
)}
|
)}
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.indexerFlags}>
|
||||||
|
{indexerFlags ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.rejected}>
|
<TableRowCell className={styles.rejected}>
|
||||||
{rejections.length ? (
|
{rejections.length ? (
|
||||||
<Popover
|
<Popover
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class OrganizePreviewModalContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && error &&
|
!isFetching && error &&
|
||||||
<div>{translate('OrganizeLoadError')}</div>
|
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function RootFolders() {
|
|||||||
|
|
||||||
if (!isFetching && !!error) {
|
if (!isFetching && !!error) {
|
||||||
return (
|
return (
|
||||||
<Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert>
|
<Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,3 +62,9 @@
|
|||||||
|
|
||||||
width: 55px;
|
width: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indexerFlags {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface CssExports {
|
|||||||
'customFormatScore': string;
|
'customFormatScore': string;
|
||||||
'episodeNumber': string;
|
'episodeNumber': string;
|
||||||
'episodeNumberAnime': string;
|
'episodeNumberAnime': string;
|
||||||
|
'indexerFlags': string;
|
||||||
'languages': string;
|
'languages': string;
|
||||||
'monitored': string;
|
'monitored': string;
|
||||||
'releaseGroup': string;
|
'releaseGroup': string;
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeNumber from 'Episode/EpisodeNumber';
|
import EpisodeNumber from 'Episode/EpisodeNumber';
|
||||||
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
||||||
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
|
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
|
import IndexerFlags from 'Episode/IndexerFlags';
|
||||||
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
||||||
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
|
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
|
||||||
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
|
||||||
import { tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import formatRuntime from 'Utilities/Number/formatRuntime';
|
import formatRuntime from 'Utilities/Number/formatRuntime';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EpisodeRow.css';
|
import styles from './EpisodeRow.css';
|
||||||
|
|
||||||
class EpisodeRow extends Component {
|
class EpisodeRow extends Component {
|
||||||
@@ -77,6 +81,7 @@ class EpisodeRow extends Component {
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
|
indexerFlags,
|
||||||
alternateTitles,
|
alternateTitles,
|
||||||
columns
|
columns
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -211,7 +216,7 @@ class EpisodeRow extends Component {
|
|||||||
customFormats.length
|
customFormats.length
|
||||||
)}
|
)}
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
@@ -322,6 +327,24 @@ class EpisodeRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'indexerFlags') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexerFlags}
|
||||||
|
>
|
||||||
|
{indexerFlags ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'status') {
|
if (name === 'status') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -381,6 +404,7 @@ EpisodeRow.propTypes = {
|
|||||||
releaseGroup: PropTypes.string,
|
releaseGroup: PropTypes.string,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
|
indexerFlags: PropTypes.number.isRequired,
|
||||||
mediaInfo: PropTypes.object,
|
mediaInfo: PropTypes.object,
|
||||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
@@ -389,7 +413,8 @@ EpisodeRow.propTypes = {
|
|||||||
|
|
||||||
EpisodeRow.defaultProps = {
|
EpisodeRow.defaultProps = {
|
||||||
alternateTitles: [],
|
alternateTitles: [],
|
||||||
customFormats: []
|
customFormats: [],
|
||||||
|
indexerFlags: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EpisodeRow;
|
export default EpisodeRow;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint max-params: 0 */
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
||||||
@@ -20,6 +19,7 @@ function createMapStateToProps() {
|
|||||||
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
|
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
|
||||||
customFormats: episodeFile ? episodeFile.customFormats : [],
|
customFormats: episodeFile ? episodeFile.customFormats : [],
|
||||||
customFormatScore: episodeFile ? episodeFile.customFormatScore : 0,
|
customFormatScore: episodeFile ? episodeFile.customFormatScore : 0,
|
||||||
|
indexerFlags: episodeFile ? episodeFile.indexerFlags : 0,
|
||||||
alternateTitles: series.alternateTitles
|
alternateTitles: series.alternateTitles
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function getExpandedState(newState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDateYear(date) {
|
function getDateYear(date) {
|
||||||
const dateDate = moment(date);
|
const dateDate = moment.utc(date);
|
||||||
|
|
||||||
return dateDate.format('YYYY');
|
return dateDate.format('YYYY');
|
||||||
}
|
}
|
||||||
@@ -428,14 +428,16 @@ class SeriesDetails extends Component {
|
|||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
name={icons.FOLDER}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.path}>
|
<div>
|
||||||
{path}
|
<Icon
|
||||||
</span>
|
name={icons.FOLDER}
|
||||||
|
size={17}
|
||||||
|
/>
|
||||||
|
<span className={styles.path}>
|
||||||
|
{path}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -444,16 +446,18 @@ class SeriesDetails extends Component {
|
|||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
name={icons.DRIVE}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.sizeOnDisk}>
|
<div>
|
||||||
{
|
<Icon
|
||||||
formatBytes(sizeOnDisk || 0)
|
name={icons.DRIVE}
|
||||||
}
|
size={17}
|
||||||
</span>
|
/>
|
||||||
|
<span className={styles.sizeOnDisk}>
|
||||||
|
{
|
||||||
|
formatBytes(sizeOnDisk || 0)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
@@ -470,32 +474,36 @@ class SeriesDetails extends Component {
|
|||||||
title={translate('QualityProfile')}
|
title={translate('QualityProfile')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
name={icons.PROFILE}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<div>
|
||||||
{
|
<Icon
|
||||||
<QualityProfileNameConnector
|
name={icons.PROFILE}
|
||||||
qualityProfileId={qualityProfileId}
|
size={17}
|
||||||
/>
|
/>
|
||||||
}
|
<span className={styles.qualityProfileName}>
|
||||||
</span>
|
{
|
||||||
|
<QualityProfileNameConnector
|
||||||
|
qualityProfileId={qualityProfileId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<div>
|
||||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
<Icon
|
||||||
</span>
|
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||||
|
size={17}
|
||||||
|
/>
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
|
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
@@ -503,14 +511,16 @@ class SeriesDetails extends Component {
|
|||||||
title={statusDetails.message}
|
title={statusDetails.message}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
name={statusDetails.icon}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<div>
|
||||||
{statusDetails.title}
|
<Icon
|
||||||
</span>
|
name={statusDetails.icon}
|
||||||
|
size={17}
|
||||||
|
/>
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
|
{statusDetails.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -520,14 +530,16 @@ class SeriesDetails extends Component {
|
|||||||
title={translate('Network')}
|
title={translate('Network')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
name={icons.NETWORK}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<div>
|
||||||
{network}
|
<Icon
|
||||||
</span>
|
name={icons.NETWORK}
|
||||||
|
size={17}
|
||||||
|
/>
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
|
{network}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,14 +549,16 @@ class SeriesDetails extends Component {
|
|||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
size={17}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.links}>
|
<div>
|
||||||
{translate('Links')}
|
<Icon
|
||||||
</span>
|
name={icons.EXTERNAL_LINK}
|
||||||
|
size={17}
|
||||||
|
/>
|
||||||
|
<span className={styles.links}>
|
||||||
|
{translate('Links')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
|
|||||||
@@ -129,10 +129,8 @@ class SeriesDetailsSeason extends Component {
|
|||||||
items
|
items
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const expand = _.some(items, (item) => {
|
const expand = _.some(items, (item) => isAfter(item.airDateUtc) || isAfter(item.airDateUtc, { days: -30 })) ||
|
||||||
return isAfter(item.airDateUtc) ||
|
items.every((item) => !item.airDateUtc);
|
||||||
isAfter(item.airDateUtc, { days: -30 });
|
|
||||||
});
|
|
||||||
|
|
||||||
onExpandPress(seasonNumber, expand && seasonNumber > 0);
|
onExpandPress(seasonNumber, expand && seasonNumber > 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 SeriesHistoryModalContentConnector from './SeriesHistoryModalContentConnector';
|
import SeriesHistoryModalContentConnector from './SeriesHistoryModalContentConnector';
|
||||||
|
|
||||||
function SeriesHistoryModal(props) {
|
function SeriesHistoryModal(props) {
|
||||||
@@ -13,6 +14,7 @@ function SeriesHistoryModal(props) {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<SeriesHistoryModalContentConnector
|
<SeriesHistoryModalContentConnector
|
||||||
|
|||||||
@@ -41,13 +41,9 @@ const columns = [
|
|||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'customFormats',
|
||||||
label: () => translate('Date'),
|
label: () => translate('CustomFormats'),
|
||||||
isVisible: true
|
isSortable: false,
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'details',
|
|
||||||
label: () => translate('Details'),
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,9 +55,13 @@ const columns = [
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
label: () => translate('Date'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: () => translate('Actions'),
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details,
|
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'details': string;
|
|
||||||
'sourceTitle': string;
|
'sourceTitle': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeNumber from 'Episode/EpisodeNumber';
|
import EpisodeNumber from 'Episode/EpisodeNumber';
|
||||||
@@ -122,11 +121,19 @@ class SeriesHistoryRow extends Component {
|
|||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
date={date}
|
date={date}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.details}>
|
<TableRowCell className={styles.actions}>
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<Icon
|
<Icon
|
||||||
@@ -144,24 +151,13 @@ class SeriesHistoryRow extends Component {
|
|||||||
}
|
}
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
|
||||||
<Tooltip
|
|
||||||
anchor={
|
|
||||||
formatCustomFormatScore(customFormatScore, customFormats.length)
|
|
||||||
}
|
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
{
|
{
|
||||||
eventType === 'grabbed' &&
|
eventType === 'grabbed' &&
|
||||||
<IconButton
|
<IconButton
|
||||||
title={translate('MarkAsFailed')}
|
title={translate('MarkAsFailed')}
|
||||||
name={icons.REMOVE}
|
name={icons.REMOVE}
|
||||||
|
size={14}
|
||||||
onPress={this.onMarkAsFailedPress}
|
onPress={this.onMarkAsFailedPress}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user