mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-18 21:55:12 -04:00
Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17e1a72baf | |||
| b454ded00a | |||
| d4512393e2 | |||
| 97d1384726 | |||
| ba002a7a4a | |||
| 349efab7a8 | |||
| af9a6f42db | |||
| 6b20fa8abd | |||
| 029ad3903f | |||
| a23d66930b | |||
| 710ab7ae09 | |||
| 434b07ae64 | |||
| eee8c95ca6 | |||
| 1f5c514011 | |||
| 66d722e097 | |||
| 39befe5aa4 | |||
| ab043e87dc | |||
| 58ae9c0a13 | |||
| 44c446943c | |||
| 8301b669fe | |||
| 6fa0b79c67 | |||
| 32d23d6636 | |||
| b31b695887 | |||
| 33de32b138 | |||
| 753b53a529 | |||
| 123535b9a5 | |||
| 7a5fa452f0 | |||
| 281e712542 | |||
| c2c34ecf53 | |||
| 615193617c | |||
| 1b58d50b6d | |||
| 99f9a0b4e6 | |||
| 696001a8bb | |||
| 31f057c097 | |||
| 0391537a60 | |||
| 521c1f760c | |||
| 3bf9b4f90f | |||
| af86a6d34e | |||
| 3ecf5c6166 | |||
| 4da3e7b2b3 | |||
| 66f38f1566 | |||
| 04b513ad14 | |||
| 1ce7fda8bb | |||
| 6d09fad675 | |||
| ad061e7ece | |||
| 3155343bcc | |||
| f5e91f7bfd | |||
| 1d69f2ed3f | |||
| 1d233dbcab | |||
| 1aafb0b201 | |||
| d7d5a2dd42 | |||
| 8060a65ef6 | |||
| 379071f838 | |||
| 5cbbd060a4 | |||
| ef19673a76 | |||
| c3cf8a6ebb | |||
| c22b27525a | |||
| eec3b01f5b | |||
| e67a127a02 | |||
| a074ebc951 | |||
| d1cd814663 | |||
| ac76646a20 | |||
| 6549f799f6 | |||
| cca55fd66c | |||
| 2f67d2813a | |||
| 9a7a5fdc38 | |||
| f1fdec6822 | |||
| 5464b23329 | |||
| 4c99971882 | |||
| cc7769b601 | |||
| cb2ed7daf9 | |||
| 78508094c8 | |||
| b0f755a30c | |||
| 9d1384792a | |||
| ea17116998 | |||
| 2c23681fc5 | |||
| 17aa2832ea | |||
| 5f3a329ef2 | |||
| 96f49da79e | |||
| c7dfde0ce9 | |||
| 8cf32020f7 | |||
| a5ed5a0e60 | |||
| 3279936fc9 | |||
| 8abccc709e | |||
| 76f30e7682 | |||
| ab289b3e42 | |||
| ef7e04065c | |||
| d1084039b3 | |||
| 7bada440d2 | |||
| 803c4752db | |||
| c0777474c0 | |||
| 66dcea5604 | |||
| a2a12d2450 | |||
| 39593bd5a8 | |||
| 45d8a8a4e6 | |||
| a4546c77ce | |||
| d69bf6360a | |||
| da9ce5b5c3 | |||
| e092098101 | |||
| 1a89a79b74 | |||
| cb6bf49922 | |||
| 4bcaba0be0 | |||
| 220ef723c7 | |||
| 9c599a6be4 | |||
| 715ce1fc6c | |||
| 8c3a192dd0 | |||
| d22bf93dfd | |||
| 886054fdf8 | |||
| 4188510586 | |||
| fedebca5e1 | |||
| e2ce6437e9 | |||
| bdae60bac9 | |||
| 2d6c818aec | |||
| a1d19852dc | |||
| 104c95f28f | |||
| 55fa1ec637 | |||
| b27a3d8272 | |||
| 089d450b46 | |||
| 358ac7434d | |||
| 9cd505fd8a | |||
| 20ac2687df | |||
| 9f075c09a2 | |||
| 3793538ba4 | |||
| 4c4b16d234 | |||
| f5790bec2e | |||
| 6c0d08de56 | |||
| ba344756b1 | |||
| dfda86aca3 | |||
| df6f83ed69 | |||
| 218d92a1ac | |||
| df2b529d01 | |||
| 0ef42dbb4d | |||
| 1a428197b2 | |||
| 09d7983845 | |||
| 6e01f3187a | |||
| 468436b9f7 | |||
| 76c288a6e4 | |||
| f95f67a7ca | |||
| 11864247eb | |||
| 74509ea7c9 | |||
| 948fe0a6dc | |||
| a4257cbcde | |||
| 2929c3c898 | |||
| 2c5f2187c8 | |||
| 401ef88971 | |||
| 4fb3754048 | |||
| 596efe8fb0 | |||
| 076a4f2574 | |||
| 9561371a47 | |||
| 16254cf5f9 | |||
| 649a03e5a0 | |||
| dd21d9b521 | |||
| 68b895d2ad | |||
| 634016ae1b | |||
| 83c6751847 | |||
| 04bb0c51b1 | |||
| d2e9621de9 | |||
| cb673ddc42 | |||
| 440618f2b6 | |||
| ae79d45664 | |||
| 1877ccb513 | |||
| b3098f2e4c | |||
| 3e0af062c1 | |||
| 858f85c50d | |||
| 938848be65 | |||
| 615f5899cc | |||
| 5a6b1313e8 | |||
| ab7debb34b | |||
| eee21de795 | |||
| 15fabbe7d0 | |||
| 6aef48c6e7 | |||
| b29bc923fc | |||
| b223e9b0cc | |||
| 77a982a7da | |||
| ab3dc765b4 | |||
| 0261201360 | |||
| 1da3954879 | |||
| 742dd5ff54 | |||
| a85406e3b7 | |||
| 73cdaf3d44 | |||
| e26fa2dbf4 | |||
| 64be68a22d | |||
| 478a185968 | |||
| 4ff5d11a03 | |||
| 6000952b76 |
+20
-8
@@ -1,19 +1,31 @@
|
|||||||
'Area: API':
|
'Area: API':
|
||||||
- src/Prowlarr.Api.V1/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/Prowlarr.Api.V1/**/*
|
||||||
|
|
||||||
'Area: Db-migration':
|
'Area: Db-migration':
|
||||||
- src/NzbDrone.Core/Datastore/Migration/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Datastore/Migration/*
|
||||||
|
|
||||||
'Area: Download Clients':
|
'Area: Download Clients':
|
||||||
- src/NzbDrone.Core/Download/Clients/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Download/Clients/**/*
|
||||||
|
|
||||||
'Area: Indexer':
|
'Area: Indexer':
|
||||||
- src/NzbDrone.Core/Indexers/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Indexers/**/*
|
||||||
|
|
||||||
'Area: Notifications':
|
'Area: Notifications':
|
||||||
- src/NzbDrone.Core/Notifications/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Notifications/**/*
|
||||||
|
|
||||||
'Area: UI':
|
'Area: UI':
|
||||||
- frontend/**/*
|
- changed-files:
|
||||||
- package.json
|
- any-glob-to-any-file:
|
||||||
- yarn.lock
|
- frontend/**/*
|
||||||
|
- package.json
|
||||||
|
- yarn.lock
|
||||||
@@ -9,4 +9,4 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v5
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v4
|
- uses: dessant/lock-threads@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Prowlarr
|
# Prowlarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||||
[](https://wiki.servarr.com/prowlarr/installation/docker)
|
[](https://wiki.servarr.com/prowlarr/installation/docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
|
|||||||
+11
-16
@@ -9,18 +9,18 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '1.16.2'
|
majorVersion: '1.24.3'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.417'
|
dotnetVersion: '6.0.424'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-11'
|
macImage: 'macOS-12'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -166,10 +166,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1075,10 +1075,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1169,7 +1169,7 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
@@ -1187,21 +1187,16 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@1
|
- task: SonarCloudAnalyze@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@4
|
- task: reportgenerator@5
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||||
- task: PublishCodeCoverageResults@1
|
publishCodeCoverageResults: true
|
||||||
displayName: Publish Coverage Report
|
|
||||||
inputs:
|
|
||||||
codeCoverageTool: 'cobertura'
|
|
||||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
|
||||||
reportDirectory: './CoverageResults/combined/'
|
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
@@ -21,17 +25,23 @@ slnFile=src/Prowlarr.sln
|
|||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
application=Prowlarr.Console.dll
|
||||||
|
else
|
||||||
|
application=Prowlarr.dll
|
||||||
|
fi
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
|
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||||
|
|
||||||
sleep 30
|
sleep 45
|
||||||
|
|
||||||
kill %1
|
kill %1
|
||||||
|
|
||||||
|
|||||||
+44
-5
@@ -357,11 +357,16 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||||
'no-shadow': 'off',
|
'@typescript-eslint/no-unused-vars': [
|
||||||
// These should be enabled after cleaning things up
|
'error',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
{
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}
|
||||||
|
],
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'react/prop-types': 'off',
|
'no-shadow': 'off',
|
||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'error',
|
'error',
|
||||||
@@ -374,7 +379,41 @@ module.exports = {
|
|||||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
// React Hooks
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
|
||||||
|
// React
|
||||||
|
'react/function-component-definition': 'error',
|
||||||
|
'react/hook-use-state': 'error',
|
||||||
|
'react/jsx-boolean-value': ['error', 'always'],
|
||||||
|
'react/jsx-curly-brace-presence': [
|
||||||
|
'error',
|
||||||
|
{ props: 'never', children: 'never' }
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': 'error',
|
||||||
|
'react/jsx-handler-names': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
eventHandlerPrefix: 'on',
|
||||||
|
eventHandlerPropPrefix: 'on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||||
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||||
|
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||||
|
'react/jsx-sort-props': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
callbacksLast: true,
|
||||||
|
noSortAlphabetically: true,
|
||||||
|
reservedFirst: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ module.exports = (env) => {
|
|||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: '[name]-[contenthash].js',
|
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ module.exports = (env) => {
|
|||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
@@ -190,7 +190,7 @@ module.exports = (env) => {
|
|||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const mixinsFiles = [
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'autoprefixer',
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { ConnectedRouter } from 'connected-react-router';
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { Store } from 'redux';
|
||||||
import PageConnector from 'Components/Page/PageConnector';
|
import PageConnector from 'Components/Page/PageConnector';
|
||||||
import ApplyTheme from './ApplyTheme';
|
import ApplyTheme from './ApplyTheme';
|
||||||
import AppRoutes from './AppRoutes';
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
function App({ store, history }) {
|
interface AppProps {
|
||||||
|
store: Store;
|
||||||
|
history: ConnectedRouterProps['history'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({ store, history }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={window.Prowlarr.instanceName}>
|
<DocumentTitle title={window.Prowlarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme>
|
<ApplyTheme />
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
</ApplyTheme>
|
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
App.propTypes = {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
|
||||||
import NotFound from 'Components/NotFound';
|
|
||||||
import Switch from 'Components/Router/Switch';
|
|
||||||
import HistoryConnector from 'History/HistoryConnector';
|
|
||||||
import IndexerIndex from 'Indexer/Index/IndexerIndex';
|
|
||||||
import IndexerStats from 'Indexer/Stats/IndexerStats';
|
|
||||||
import SearchIndexConnector from 'Search/SearchIndexConnector';
|
|
||||||
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
|
|
||||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
|
||||||
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
|
||||||
import Settings from 'Settings/Settings';
|
|
||||||
import TagSettings from 'Settings/Tags/TagSettings';
|
|
||||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
|
||||||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
|
||||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|
||||||
import Logs from 'System/Logs/Logs';
|
|
||||||
import Status from 'System/Status/Status';
|
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
|
||||||
|
|
||||||
function AppRoutes(props) {
|
|
||||||
const {
|
|
||||||
app
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{/*
|
|
||||||
Indexers
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
component={IndexerIndex}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
window.Prowlarr.urlBase &&
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
addUrlBase={false}
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={getPathWithUrlBase('/')}
|
|
||||||
component={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/indexers/stats"
|
|
||||||
component={IndexerStats}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Search
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/search"
|
|
||||||
component={SearchIndexConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Activity
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/history"
|
|
||||||
component={HistoryConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Settings
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/settings"
|
|
||||||
component={Settings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/indexers"
|
|
||||||
component={IndexerSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/applications"
|
|
||||||
component={ApplicationSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/downloadclients"
|
|
||||||
component={DownloadClientSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/connect"
|
|
||||||
component={NotificationSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/tags"
|
|
||||||
component={TagSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/general"
|
|
||||||
component={GeneralSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/ui"
|
|
||||||
component={UISettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/development"
|
|
||||||
component={DevelopmentSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
System
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/status"
|
|
||||||
component={Status}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/tasks"
|
|
||||||
component={Tasks}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/backup"
|
|
||||||
component={BackupsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/updates"
|
|
||||||
component={UpdatesConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/events"
|
|
||||||
component={LogsTableConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/logs/files"
|
|
||||||
component={Logs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Not Found
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="*"
|
|
||||||
component={NotFound}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppRoutes.propTypes = {
|
|
||||||
app: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppRoutes;
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
|
import NotFound from 'Components/NotFound';
|
||||||
|
import Switch from 'Components/Router/Switch';
|
||||||
|
import HistoryConnector from 'History/HistoryConnector';
|
||||||
|
import IndexerIndex from 'Indexer/Index/IndexerIndex';
|
||||||
|
import IndexerStats from 'Indexer/Stats/IndexerStats';
|
||||||
|
import SearchIndexConnector from 'Search/SearchIndexConnector';
|
||||||
|
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
|
||||||
|
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||||
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
|
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||||
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
|
import Settings from 'Settings/Settings';
|
||||||
|
import TagSettings from 'Settings/Tags/TagSettings';
|
||||||
|
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||||
|
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||||
|
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
|
import Logs from 'System/Logs/Logs';
|
||||||
|
import Status from 'System/Status/Status';
|
||||||
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
|
||||||
|
function RedirectWithUrlBase() {
|
||||||
|
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{/*
|
||||||
|
Indexers
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/" component={IndexerIndex} />
|
||||||
|
|
||||||
|
{window.Prowlarr.urlBase && (
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
addUrlBase={false}
|
||||||
|
render={RedirectWithUrlBase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Route path="/indexers/stats" component={IndexerStats} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Search
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/search" component={SearchIndexConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Activity
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/history" component={HistoryConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Settings
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/settings" component={Settings} />
|
||||||
|
|
||||||
|
<Route path="/settings/indexers" component={IndexerSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/applications" component={ApplicationSettings} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/downloadclients"
|
||||||
|
component={DownloadClientSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/connect" component={NotificationSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/tags" component={TagSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/general" component={GeneralSettingsConnector} />
|
||||||
|
|
||||||
|
<Route path="/settings/ui" component={UISettingsConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/development"
|
||||||
|
component={DevelopmentSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
System
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/system/status" component={Status} />
|
||||||
|
|
||||||
|
<Route path="/system/tasks" component={Tasks} />
|
||||||
|
|
||||||
|
<Route path="/system/backup" component={BackupsConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/updates" component={UpdatesConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/events" component={LogsTableConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/logs/files" component={Logs} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Not Found
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="*" component={NotFound} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector';
|
|
||||||
|
|
||||||
function AppUpdatedModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnBackgroundClick={false}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AppUpdatedModalContentConnector
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModal;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
||||||
|
|
||||||
|
interface AppUpdatedModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModal(props: AppUpdatedModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AppUpdatedModalContent onModalClose={handleModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import AppUpdatedModal from './AppUpdatedModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(AppUpdatedModal);
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AppUpdatedModalContent.css';
|
|
||||||
|
|
||||||
function mergeUpdates(items, version, prevVersion) {
|
|
||||||
let installedIndex = items.findIndex((u) => u.version === version);
|
|
||||||
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
|
|
||||||
|
|
||||||
if (installedIndex === -1) {
|
|
||||||
installedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (installedPreviouslyIndex === -1) {
|
|
||||||
installedPreviouslyIndex = items.length;
|
|
||||||
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
|
||||||
installedPreviouslyIndex += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
|
||||||
|
|
||||||
if (!appliedUpdates.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appliedChanges = { new: [], fixed: [] };
|
|
||||||
appliedUpdates.forEach((u) => {
|
|
||||||
if (u.changes) {
|
|
||||||
appliedChanges.new.push(... u.changes.new);
|
|
||||||
appliedChanges.fixed.push(... u.changes.fixed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
|
|
||||||
|
|
||||||
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
|
||||||
mergedUpdate.changes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppUpdatedModalContent(props) {
|
|
||||||
const {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
onSeeChangesPress,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const update = mergeUpdates(items, version, prevVersion);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AppUpdated')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !!update &&
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
!update.changes &&
|
|
||||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!update.changes &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.changes}>
|
|
||||||
{translate('WhatsNew')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('New')}
|
|
||||||
changes={update.changes.new}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('Fixed')}
|
|
||||||
changes={update.changes.fixed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isPopulated && !error &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onSeeChangesPress}
|
|
||||||
>
|
|
||||||
{translate('RecentChanges')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContent.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
prevVersion: PropTypes.string,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSeeChangesPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModalContent;
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||||
|
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
import styles from './AppUpdatedModalContent.css';
|
||||||
|
|
||||||
|
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
|
||||||
|
let installedIndex = items.findIndex((u) => u.version === version);
|
||||||
|
let installedPreviouslyIndex = items.findIndex(
|
||||||
|
(u) => u.version === prevVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (installedIndex === -1) {
|
||||||
|
installedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installedPreviouslyIndex === -1) {
|
||||||
|
installedPreviouslyIndex = items.length;
|
||||||
|
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
||||||
|
installedPreviouslyIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
||||||
|
|
||||||
|
if (!appliedUpdates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedChanges: Update['changes'] = { new: [], fixed: [] };
|
||||||
|
|
||||||
|
appliedUpdates.forEach((u: Update) => {
|
||||||
|
if (u.changes) {
|
||||||
|
appliedChanges.new.push(...u.changes.new);
|
||||||
|
appliedChanges.fixed.push(...u.changes.fixed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
|
||||||
|
changes: appliedChanges,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||||
|
mergedUpdate.changes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppUpdatedModalContentProps {
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { version, prevVersion } = useSelector((state: AppState) => state.app);
|
||||||
|
const { isPopulated, error, items } = useSelector(
|
||||||
|
(state: AppState) => state.system.updates
|
||||||
|
);
|
||||||
|
const previousVersion = usePrevious(version);
|
||||||
|
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const update = mergeUpdates(items, version, prevVersion);
|
||||||
|
|
||||||
|
const handleSeeChangesPress = useCallback(() => {
|
||||||
|
window.location.href = `${window.Prowlarr.urlBase}/system/updates`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (version !== previousVersion) {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}
|
||||||
|
}, [version, previousVersion, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('AppUpdated')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('AppUpdatedVersion', { version })}
|
||||||
|
blockClassName={styles.version}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPopulated && !error && !!update ? (
|
||||||
|
<div>
|
||||||
|
{update.changes ? (
|
||||||
|
<div className={styles.maintenance}>
|
||||||
|
{translate('MaintenanceRelease')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{update.changes ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.changes}>{translate('WhatsNew')}</div>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('New')}
|
||||||
|
changes={update.changes.new}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('Fixed')}
|
||||||
|
changes={update.changes.fixed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleSeeChangesPress}>
|
||||||
|
{translate('RecentChanges')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onModalClose}>
|
||||||
|
{translate('Reload')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModalContent;
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
|
||||||
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app.version,
|
|
||||||
(state) => state.app.prevVersion,
|
|
||||||
(state) => state.system.updates,
|
|
||||||
(version, prevVersion, updates) => {
|
|
||||||
const {
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = updates;
|
|
||||||
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
dispatchFetchUpdates() {
|
|
||||||
dispatch(fetchUpdates());
|
|
||||||
},
|
|
||||||
|
|
||||||
onSeeChangesPress() {
|
|
||||||
window.location = `${window.Prowlarr.urlBase}/system/updates`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppUpdatedModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.version !== this.props.version) {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatchFetchUpdates,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppUpdatedModalContent {...otherProps} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContentConnector.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
dispatchFetchUpdates: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import themes from 'Styles/Themes';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
|
||||||
(
|
|
||||||
theme
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
theme
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplyTheme({ theme, children }) {
|
|
||||||
// Update the CSS Variables
|
|
||||||
|
|
||||||
const updateCSSVariables = useCallback(() => {
|
|
||||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
|
||||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
|
||||||
|
|
||||||
// Loop through each array key and set the CSS Variables
|
|
||||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
|
||||||
// Based on our snippet from MDN
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
`--${cssVariableKey}`,
|
|
||||||
arrayOfVariableValues[index]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// On Component Mount and Component Update
|
|
||||||
useEffect(() => {
|
|
||||||
updateCSSVariables(theme);
|
|
||||||
}, [updateCSSVariables, theme]);
|
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyTheme.propTypes = {
|
|
||||||
theme: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
|
||||||
|
function createThemeSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||||
|
(theme) => {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme() {
|
||||||
|
const theme = useSelector(createThemeSelector());
|
||||||
|
|
||||||
|
const updateCSSVariables = useCallback(() => {
|
||||||
|
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||||
|
document.documentElement.style.setProperty(`--${key}`, value);
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
// On Component Mount and Component Update
|
||||||
|
useEffect(() => {
|
||||||
|
updateCSSVariables();
|
||||||
|
}, [updateCSSVariables, theme]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplyTheme;
|
||||||
+16
-27
@@ -1,5 +1,4 @@
|
|||||||
import PropTypes from 'prop-types';
|
import React, { useCallback } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
@@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ConnectionLostModal.css';
|
import styles from './ConnectionLostModal.css';
|
||||||
|
|
||||||
function ConnectionLostModal(props) {
|
interface ConnectionLostModalProps {
|
||||||
const {
|
isOpen: boolean;
|
||||||
isOpen,
|
}
|
||||||
onModalClose
|
|
||||||
} = props;
|
function ConnectionLostModal(props: ConnectionLostModalProps) {
|
||||||
|
const { isOpen } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
isOpen={isOpen}
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
onModalClose={onModalClose}
|
<ModalHeader>{translate('ConnectionLost')}</ModalHeader>
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('ConnectionLost')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>{translate('ConnectionLostToBackend')}</div>
|
||||||
{translate('ConnectionLostToBackend')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostReconnect')}
|
{translate('ConnectionLostReconnect')}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button kind={kinds.PRIMARY} onPress={handleModalClose}>
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
{translate('Reload')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -48,9 +42,4 @@ function ConnectionLostModal(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionLostModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectionLostModal;
|
export default ConnectionLostModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ConnectionLostModal from './ConnectionLostModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import { FilterBuilderProp } from './AppState';
|
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
@@ -18,10 +19,18 @@ export interface AppSectionSaveState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagedAppSectionState {
|
export interface PagedAppSectionState {
|
||||||
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalRecords?: number;
|
||||||
|
}
|
||||||
|
export interface TableAppSectionState {
|
||||||
|
columns: Column[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionFilterState<T> {
|
export interface AppSectionFilterState<T> {
|
||||||
|
selectedFilterKey: string;
|
||||||
|
filters: PropertyFilter[];
|
||||||
filterBuilderProps: FilterBuilderProp<T>[];
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +47,7 @@ export interface AppSectionItemState<T> {
|
|||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,20 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
indexerHistory: IndexerHistoryAppState;
|
indexerHistory: IndexerHistoryAppState;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ interface IndexerAppState
|
|||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState {
|
||||||
itemMap: Record<number, number>;
|
itemMap: Record<number, number>;
|
||||||
|
|
||||||
|
isTestingAll: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export interface ApplicationAppState
|
|||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IndexerCategoryAppState
|
export interface IndexerCategoryAppState
|
||||||
extends AppSectionState<IndexerCategory>,
|
extends AppSectionState<IndexerCategory>,
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
|
import Health from 'typings/Health';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import { AppSectionItemState } from './AppSectionState';
|
import Task from 'typings/Task';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
|
health: HealthAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
|
tasks: TaskAppState;
|
||||||
|
updates: UpdateAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemAppState;
|
export default SystemAppState;
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ class StackedBarChart extends Component {
|
|||||||
size: 14,
|
size: 14,
|
||||||
family: defaultFontFamily
|
family: defaultFontFamily
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
position: 'average'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
descriptionClassName,
|
descriptionClassName,
|
||||||
title,
|
title,
|
||||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
<DescriptionListItemTitle
|
<DescriptionListItemTitle
|
||||||
className={titleClassName}
|
className={titleClassName}
|
||||||
>
|
>
|
||||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DescriptionListItem.propTypes = {
|
DescriptionListItem.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
titleClassName: PropTypes.string,
|
titleClassName: PropTypes.string,
|
||||||
descriptionClassName: PropTypes.string,
|
descriptionClassName: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
|||||||
<div>{info.componentStack}</div>
|
<div>{info.componentStack}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{
|
<div className={styles.version}>Version: {window.Prowlarr.version}</div>
|
||||||
<div className={styles.version}>
|
|
||||||
Version: {window.Prowlarr.version}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
||||||
@@ -212,7 +213,7 @@ class FilterBuilderRow extends Component {
|
|||||||
key: name,
|
key: name,
|
||||||
value: typeof label === 'function' ? label() : label
|
value: typeof label === 'function' ? label() : label
|
||||||
};
|
};
|
||||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
}).sort(sortByProp('value'));
|
||||||
|
|
||||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { filterBuilderTypes } from 'Helpers/Props';
|
import { filterBuilderTypes } from 'Helpers/Props';
|
||||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
function createTagListSelector() {
|
function createTagListSelector() {
|
||||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []).sort(sortByName);
|
}, []).sort(sortByProp('name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.uniqBy(items, 'id');
|
return _.uniqBy(items, 'id');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CustomFilter from './CustomFilter';
|
import CustomFilter from './CustomFilter';
|
||||||
import styles from './CustomFiltersModalContent.css';
|
import styles from './CustomFiltersModalContent.css';
|
||||||
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||||
.map((customFilter) => {
|
.map((customFilter) => {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(appProfiles, includeNoChange, includeMixed) => {
|
(appProfiles, includeNoChange, includeMixed) => {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -21,7 +22,7 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
const values = items
|
const values = items
|
||||||
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
||||||
.sort(sortByName)
|
.sort(sortByProp('name'))
|
||||||
.map((downloadClient) => ({
|
.map((downloadClient) => ({
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name,
|
value: downloadClient.name,
|
||||||
@@ -31,7 +32,7 @@ function createMapStateToProps() {
|
|||||||
if (includeAny) {
|
if (includeAny) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 0,
|
key: 0,
|
||||||
value: '(Any)'
|
value: `(${translate('Any')})`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,26 +271,29 @@ class EnhancedSelectInput extends Component {
|
|||||||
this.setState({ isOpen: !this.state.isOpen });
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelect = (value) => {
|
onSelect = (newValue) => {
|
||||||
if (Array.isArray(this.props.value)) {
|
const { name, value, values, onChange } = this.props;
|
||||||
let newValue = null;
|
|
||||||
const index = this.props.value.indexOf(value);
|
if (Array.isArray(value)) {
|
||||||
|
let arrayValue = null;
|
||||||
|
const index = value.indexOf(newValue);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
||||||
} else {
|
} else {
|
||||||
newValue = [...this.props.value];
|
arrayValue = [...value];
|
||||||
newValue.splice(index, 1);
|
arrayValue.splice(index, 1);
|
||||||
}
|
}
|
||||||
this.props.onChange({
|
onChange({
|
||||||
name: this.props.name,
|
name,
|
||||||
value: newValue
|
value: arrayValue
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ isOpen: false });
|
this.setState({ isOpen: false });
|
||||||
|
|
||||||
this.props.onChange({
|
onChange({
|
||||||
name: this.props.name,
|
name,
|
||||||
value
|
value: newValue
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -485,7 +488,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
values.map((v, index) => {
|
values.map((v, index) => {
|
||||||
const hasParent = v.parentKey !== undefined;
|
const hasParent = v.parentKey !== undefined;
|
||||||
const depth = hasParent ? 1 : 0;
|
const depth = hasParent ? 1 : 0;
|
||||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
||||||
return (
|
return (
|
||||||
<OptionComponent
|
<OptionComponent
|
||||||
key={v.key}
|
key={v.key}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ function getSelectOptions(items) {
|
|||||||
key: option.value,
|
key: option.value,
|
||||||
value: option.name,
|
value: option.name,
|
||||||
hint: option.hint,
|
hint: option.hint,
|
||||||
parentKey: option.parentValue
|
parentKey: option.parentValue,
|
||||||
|
isDisabled: option.isDisabled
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
.validationFailures {
|
.validationFailures {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'details': string;
|
||||||
'validationFailures': string;
|
'validationFailures': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import { kinds } from 'Helpers/Props';
|
import Icon from 'Components/Icon';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import styles from './Form.css';
|
import styles from './Form.css';
|
||||||
|
|
||||||
function Form(props) {
|
function Form(props) {
|
||||||
@@ -26,6 +28,18 @@ function Form(props) {
|
|||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{error.errorMessage}
|
{error.errorMessage}
|
||||||
|
|
||||||
|
{
|
||||||
|
error.detailedDescription ?
|
||||||
|
<Tooltip
|
||||||
|
className={styles.details}
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={error.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -39,6 +53,18 @@ function Form(props) {
|
|||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
{warning.errorMessage}
|
{warning.errorMessage}
|
||||||
|
|
||||||
|
{
|
||||||
|
warning.detailedDescription ?
|
||||||
|
<Tooltip
|
||||||
|
className={styles.details}
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={warning.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -256,6 +256,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),
|
||||||
|
isFloat: 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,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function FormInputHelpText(props) {
|
|||||||
isCheckInput && styles.isCheckInput
|
isCheckInput && styles.isCheckInput
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div dangerouslySetInnerHTML={{ __html: text }} />
|
{text}
|
||||||
|
|
||||||
{
|
{
|
||||||
link ?
|
link ?
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
createSortedSectionSelector('indexers', sortByName),
|
createSortedSectionSelector('indexers', sortByProp('name')),
|
||||||
(value, indexers) => {
|
(value, indexers) => {
|
||||||
const values = [];
|
const values = [];
|
||||||
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
.input {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
font-family: $passwordFamily;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './PasswordInput.css';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
function onCopy(e) {
|
function onCopy(e) {
|
||||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
{...props}
|
{...props}
|
||||||
|
type="password"
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
className: PropTypes.string.isRequired
|
...TextInput.props
|
||||||
};
|
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
|
||||||
className: styles.input
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordInput;
|
export default PasswordInput;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
.select {
|
.select {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
composes: input from '~Components/Form/Input.css';
|
composes: input from '~Components/Form/Input.css';
|
||||||
|
|
||||||
padding: 0 11px;
|
padding: 0 30px 0 11px;
|
||||||
|
background-image: none, linear-gradient(-135deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(-225deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(var(--inputBackgroundColor) 42%, var(--textColor) 42%);
|
||||||
|
background-position: right 30px center, right bottom, right bottom, right bottom;
|
||||||
|
background-size: 1px 100%, 35px 27px, 30px 35px, 30px 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterMenuItem from './FilterMenuItem';
|
import FilterMenuItem from './FilterMenuItem';
|
||||||
import MenuContent from './MenuContent';
|
import MenuContent from './MenuContent';
|
||||||
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort(sortByProp('label'))
|
||||||
.map((filter) => {
|
.map((filter) => {
|
||||||
return (
|
return (
|
||||||
<FilterMenuItem
|
<FilterMenuItem
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
|
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
|
||||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||||
import styles from './PageHeader.css';
|
import styles from './PageHeader.css';
|
||||||
|
|
||||||
class PageHeader extends Component {
|
class PageHeader extends Component {
|
||||||
@@ -87,7 +87,8 @@ class PageHeader extends Component {
|
|||||||
to="https://translate.servarr.com/projects/servarr/prowlarr/"
|
to="https://translate.servarr.com/projects/servarr/prowlarr/"
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
<PageHeaderActionsMenuConnector
|
|
||||||
|
<PageHeaderActionsMenu
|
||||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Menu from 'Components/Menu/Menu';
|
|
||||||
import MenuButton from 'Components/Menu/MenuButton';
|
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import MenuItem from 'Components/Menu/MenuItem';
|
|
||||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './PageHeaderActionsMenu.css';
|
|
||||||
|
|
||||||
function PageHeaderActionsMenu(props) {
|
|
||||||
const {
|
|
||||||
formsAuth,
|
|
||||||
onKeyboardShortcutsPress,
|
|
||||||
onRestartPress,
|
|
||||||
onShutdownPress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Menu alignMenu={align.RIGHT}>
|
|
||||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
|
||||||
<Icon
|
|
||||||
name={icons.INTERACTIVE}
|
|
||||||
title={translate('Menu')}
|
|
||||||
/>
|
|
||||||
</MenuButton>
|
|
||||||
|
|
||||||
<MenuContent>
|
|
||||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.KEYBOARD}
|
|
||||||
/>
|
|
||||||
{translate('KeyboardShortcuts')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItemSeparator />
|
|
||||||
|
|
||||||
<MenuItem onPress={onRestartPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.RESTART}
|
|
||||||
/>
|
|
||||||
{translate('Restart')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem onPress={onShutdownPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.SHUTDOWN}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
/>
|
|
||||||
{translate('Shutdown')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
{
|
|
||||||
formsAuth &&
|
|
||||||
<div className={styles.separator} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
formsAuth &&
|
|
||||||
<MenuItem
|
|
||||||
to={`${window.Prowlarr.urlBase}/logout`}
|
|
||||||
noRouter={true}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.LOGOUT}
|
|
||||||
/>
|
|
||||||
Logout
|
|
||||||
</MenuItem>
|
|
||||||
}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PageHeaderActionsMenu.propTypes = {
|
|
||||||
formsAuth: PropTypes.bool.isRequired,
|
|
||||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
|
||||||
onRestartPress: PropTypes.func.isRequired,
|
|
||||||
onShutdownPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageHeaderActionsMenu;
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Menu from 'Components/Menu/Menu';
|
||||||
|
import MenuButton from 'Components/Menu/MenuButton';
|
||||||
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
|
import MenuItem from 'Components/Menu/MenuItem';
|
||||||
|
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './PageHeaderActionsMenu.css';
|
||||||
|
|
||||||
|
interface PageHeaderActionsMenuProps {
|
||||||
|
onKeyboardShortcutsPress(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||||
|
const { onKeyboardShortcutsPress } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { authentication, isDocker } = useSelector(
|
||||||
|
(state: AppState) => state.system.status.item
|
||||||
|
);
|
||||||
|
|
||||||
|
const formsAuth = authentication === 'forms';
|
||||||
|
|
||||||
|
const handleRestartPress = useCallback(() => {
|
||||||
|
dispatch(restart());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleShutdownPress = useCallback(() => {
|
||||||
|
dispatch(shutdown());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Menu alignMenu={align.RIGHT}>
|
||||||
|
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||||
|
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<MenuContent>
|
||||||
|
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
||||||
|
{translate('KeyboardShortcuts')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{isDocker ? null : (
|
||||||
|
<>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
|
||||||
|
<MenuItem onPress={handleRestartPress}>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
||||||
|
{translate('Restart')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onPress={handleShutdownPress}>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.SHUTDOWN}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
/>
|
||||||
|
{translate('Shutdown')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formsAuth ? (
|
||||||
|
<>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
to={`${window.Prowlarr.urlBase}/logout`}
|
||||||
|
noRouter={true}
|
||||||
|
>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
||||||
|
{translate('Logout')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeaderActionsMenu;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
|
||||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.system.status,
|
|
||||||
(status) => {
|
|
||||||
return {
|
|
||||||
formsAuth: status.item.authentication === 'forms'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
restart,
|
|
||||||
shutdown
|
|
||||||
};
|
|
||||||
|
|
||||||
class PageHeaderActionsMenuConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRestartPress = () => {
|
|
||||||
this.props.restart();
|
|
||||||
};
|
|
||||||
|
|
||||||
onShutdownPress = () => {
|
|
||||||
this.props.shutdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageHeaderActionsMenu
|
|
||||||
{...this.props}
|
|
||||||
onRestartPress={this.onRestartPress}
|
|
||||||
onShutdownPress={this.onShutdownPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PageHeaderActionsMenuConnector.propTypes = {
|
|
||||||
restart: PropTypes.func.isRequired,
|
|
||||||
shutdown: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
import AppUpdatedModal from 'App/AppUpdatedModal';
|
||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModal from 'App/ConnectionLostModal';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
@@ -102,12 +102,12 @@ class Page extends Component {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppUpdatedModalConnector
|
<AppUpdatedModal
|
||||||
isOpen={this.state.isUpdatedModalOpen}
|
isOpen={this.state.isUpdatedModalOpen}
|
||||||
onModalClose={this.onUpdatedModalClose}
|
onModalClose={this.onUpdatedModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConnectionLostModalConnector
|
<ConnectionLostModal
|
||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller';
|
|||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
|
import HealthStatus from 'System/Status/Health/HealthStatus';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MessagesConnector from './Messages/MessagesConnector';
|
import MessagesConnector from './Messages/MessagesConnector';
|
||||||
import PageSidebarItem from './PageSidebarItem';
|
import PageSidebarItem from './PageSidebarItem';
|
||||||
@@ -87,7 +87,7 @@ const links = [
|
|||||||
{
|
{
|
||||||
title: () => translate('Status'),
|
title: () => translate('Status'),
|
||||||
to: '/system/status',
|
to: '/system/status',
|
||||||
statusComponent: HealthStatusConnector
|
statusComponent: HealthStatus
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: () => translate('Tasks'),
|
title: () => translate('Tasks'),
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import React from 'react';
|
|||||||
|
|
||||||
type PropertyFunction<T> = () => T;
|
type PropertyFunction<T> = () => T;
|
||||||
|
|
||||||
|
// TODO: Convert to generic so `name` can be a type
|
||||||
interface Column {
|
interface Column {
|
||||||
name: string;
|
name: string;
|
||||||
label: string | PropertyFunction<string> | React.ReactNode;
|
label: string | PropertyFunction<string> | React.ReactNode;
|
||||||
|
className?: string;
|
||||||
columnLabel?: string;
|
columnLabel?: string;
|
||||||
isSortable?: boolean;
|
isSortable?: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
interface PagingOptions {
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
|
gotoPage: ({ page }: { page: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePaging(options: PagingOptions) {
|
||||||
|
const { page, totalPages, gotoPage } = options;
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleFirstPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: 1 }));
|
||||||
|
}, [dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handlePreviousPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: Math.max(page - 1, 1) }));
|
||||||
|
}, [page, dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handleNextPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: Math.min(page + 1, totalPages) }));
|
||||||
|
}, [page, totalPages, dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handleLastPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: totalPages }));
|
||||||
|
}, [totalPages, dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handlePageSelect = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
dispatch(gotoPage({ page }));
|
||||||
|
},
|
||||||
|
[dispatch, gotoPage]
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePaging;
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
import styles from './TagList.css';
|
import styles from './TagList.css';
|
||||||
|
|
||||||
function TagList({ tags, tagList }) {
|
function TagList({ tags, tagList }) {
|
||||||
const sortedTags = tags
|
const sortedTags = tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((t) => t !== undefined)
|
.filter((tag) => !!tag)
|
||||||
.sort((a, b) => a.label.localeCompare(b.label));
|
.sort(sortByProp('label'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
|
|||||||
@@ -25,14 +25,3 @@
|
|||||||
font-family: 'Ubuntu Mono';
|
font-family: 'Ubuntu Mono';
|
||||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* text-security-disc
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-family: 'text-security-disc';
|
|
||||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
|
||||||
|
|
||||||
|
export default DownloadProtocol;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
function useCurrentPage() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
return history.action === 'POP';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCurrentPage;
|
||||||
@@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
|
|||||||
export default function useModalOpenState(
|
export default function useModalOpenState(
|
||||||
initialState: boolean
|
initialState: boolean
|
||||||
): [boolean, () => void, () => void] {
|
): [boolean, () => void, () => void] {
|
||||||
const [isOpen, setOpen] = useState(initialState);
|
const [isOpen, setIsOpen] = useState(initialState);
|
||||||
|
|
||||||
const setModalOpen = useCallback(() => {
|
const setModalOpen = useCallback(() => {
|
||||||
setOpen(true);
|
setIsOpen(true);
|
||||||
}, [setOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
const setModalClosed = useCallback(() => {
|
const setModalClosed = useCallback(() => {
|
||||||
setOpen(false);
|
setIsOpen(false);
|
||||||
}, [setOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
return [isOpen, setModalOpen, setModalClosed];
|
return [isOpen, setModalOpen, setModalClosed];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
|
||||||
|
|
||||||
|
export default TooltipPosition;
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
faChevronCircleRight as fasChevronCircleRight,
|
faChevronCircleRight as fasChevronCircleRight,
|
||||||
faChevronCircleUp as fasChevronCircleUp,
|
faChevronCircleUp as fasChevronCircleUp,
|
||||||
faCircle as fasCircle,
|
faCircle as fasCircle,
|
||||||
|
faCircleDown as fasCircleDown,
|
||||||
faCloud as fasCloud,
|
faCloud as fasCloud,
|
||||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||||
faCog as fasCog,
|
faCog as fasCog,
|
||||||
@@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
|||||||
export const CHECK_CIRCLE = fasCheckCircle;
|
export const CHECK_CIRCLE = fasCheckCircle;
|
||||||
export const CHECK_SQUARE = fasSquareCheck;
|
export const CHECK_SQUARE = fasSquareCheck;
|
||||||
export const CIRCLE = fasCircle;
|
export const CIRCLE = fasCircle;
|
||||||
|
export const CIRCLE_DOWN = fasCircleDown;
|
||||||
export const CIRCLE_OUTLINE = farCircle;
|
export const CIRCLE_OUTLINE = farCircle;
|
||||||
export const CLEAR = fasTrashAlt;
|
export const CLEAR = fasTrashAlt;
|
||||||
export const CLIPBOARD = fasCopy;
|
export const CLIPBOARD = fasCopy;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryDetails.css';
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
@@ -10,7 +11,10 @@ function HistoryDetails(props) {
|
|||||||
const {
|
const {
|
||||||
indexer,
|
indexer,
|
||||||
eventType,
|
eventType,
|
||||||
data
|
date,
|
||||||
|
data,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
|
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
|
||||||
@@ -21,7 +25,10 @@ function HistoryDetails(props) {
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
source,
|
source,
|
||||||
url
|
host,
|
||||||
|
url,
|
||||||
|
elapsedTime,
|
||||||
|
cached
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,6 +93,15 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Host')}
|
||||||
|
data={host}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data ?
|
data ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -94,6 +110,24 @@ function HistoryDetails(props) {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
elapsedTime ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('ElapsedTime')}
|
||||||
|
data={`${elapsedTime}ms${cached === '1' ? ' (cached)' : ''}`}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,10 +135,19 @@ function HistoryDetails(props) {
|
|||||||
if (eventType === 'releaseGrabbed') {
|
if (eventType === 'releaseGrabbed') {
|
||||||
const {
|
const {
|
||||||
source,
|
source,
|
||||||
|
host,
|
||||||
grabTitle,
|
grabTitle,
|
||||||
url
|
url,
|
||||||
|
publishedDate,
|
||||||
|
infoUrl,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientName,
|
||||||
|
elapsedTime,
|
||||||
|
grabMethod
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
{
|
{
|
||||||
@@ -125,6 +168,15 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Host')}
|
||||||
|
data={host}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data ?
|
data ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -134,6 +186,33 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
infoUrl ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('InfoUrl')}
|
||||||
|
data={<Link to={infoUrl}>{infoUrl}</Link>}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
publishedDate ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('PublishedDate')}
|
||||||
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
downloadClientNameInfo ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={downloadClientNameInfo}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data ?
|
data ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -142,11 +221,40 @@ function HistoryDetails(props) {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
elapsedTime ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('ElapsedTime')}
|
||||||
|
data={`${elapsedTime}ms`}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
grabMethod ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Redirected')}
|
||||||
|
data={grabMethod.toLowerCase() === 'redirect' ? translate('Yes') : translate('No')}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'indexerAuth') {
|
if (eventType === 'indexerAuth') {
|
||||||
|
const { elapsedTime } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
@@ -160,6 +268,24 @@ function HistoryDetails(props) {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
elapsedTime ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('ElapsedTime')}
|
||||||
|
data={`${elapsedTime}ms`}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -171,6 +297,15 @@ function HistoryDetails(props) {
|
|||||||
title={translate('Name')}
|
title={translate('Name')}
|
||||||
data={data.query}
|
data={data.query}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -178,6 +313,7 @@ function HistoryDetails(props) {
|
|||||||
HistoryDetails.propTypes = {
|
HistoryDetails.propTypes = {
|
||||||
indexer: PropTypes.object.isRequired,
|
indexer: PropTypes.object.isRequired,
|
||||||
eventType: PropTypes.string.isRequired,
|
eventType: PropTypes.string.isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired
|
timeFormat: PropTypes.string.isRequired
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function HistoryDetailsModal(props) {
|
|||||||
isOpen,
|
isOpen,
|
||||||
eventType,
|
eventType,
|
||||||
indexer,
|
indexer,
|
||||||
|
date,
|
||||||
data,
|
data,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
@@ -49,6 +50,7 @@ function HistoryDetailsModal(props) {
|
|||||||
<HistoryDetails
|
<HistoryDetails
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
indexer={indexer}
|
indexer={indexer}
|
||||||
|
date={date}
|
||||||
data={data}
|
data={data}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
@@ -71,6 +73,7 @@ HistoryDetailsModal.propTypes = {
|
|||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
eventType: PropTypes.string.isRequired,
|
eventType: PropTypes.string.isRequired,
|
||||||
indexer: PropTypes.object.isRequired,
|
indexer: PropTypes.object.isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -20,21 +20,22 @@ function getIconName(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconKind(successful) {
|
function getIconKind(successful, redirect) {
|
||||||
switch (successful) {
|
if (redirect) {
|
||||||
case false:
|
return kinds.INFO;
|
||||||
return kinds.DANGER;
|
} else if (!successful) {
|
||||||
default:
|
return kinds.DANGER;
|
||||||
return kinds.DEFAULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return kinds.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTooltip(eventType, data, indexer) {
|
function getTooltip(eventType, data, indexer, redirect) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'indexerQuery':
|
case 'indexerQuery':
|
||||||
return `Query "${data.query}" sent to ${indexer.name}`;
|
return `Query "${data.query}" sent to ${indexer.name}`;
|
||||||
case 'releaseGrabbed':
|
case 'releaseGrabbed':
|
||||||
return `Release grabbed from ${indexer.name}`;
|
return redirect ? `Release grabbed via redirect from ${indexer.name}` : `Release grabbed from ${indexer.name}`;
|
||||||
case 'indexerAuth':
|
case 'indexerAuth':
|
||||||
return `Auth attempted for ${indexer.name}`;
|
return `Auth attempted for ${indexer.name}`;
|
||||||
case 'indexerRss':
|
case 'indexerRss':
|
||||||
@@ -45,9 +46,12 @@ function getTooltip(eventType, data, indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
|
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
|
||||||
|
const { grabMethod } = data;
|
||||||
|
const redirect = grabMethod && grabMethod.toLowerCase() === 'redirect';
|
||||||
|
|
||||||
const iconName = getIconName(eventType);
|
const iconName = getIconName(eventType);
|
||||||
const iconKind = getIconKind(successful);
|
const iconKind = getIconKind(successful, redirect);
|
||||||
const tooltip = getTooltip(eventType, data, indexer);
|
const tooltip = getTooltip(eventType, data, indexer, redirect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
@@ -331,6 +331,21 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'host') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexer}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
data.host ?
|
||||||
|
data.host :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'elapsedTime') {
|
if (name === 'elapsedTime') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -355,8 +370,9 @@ class HistoryRow extends Component {
|
|||||||
return (
|
return (
|
||||||
<RelativeDateCell
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={date}
|
|
||||||
className={styles.date}
|
className={styles.date}
|
||||||
|
date={date}
|
||||||
|
includeSeconds={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -393,6 +409,7 @@ class HistoryRow extends Component {
|
|||||||
<HistoryDetailsModal
|
<HistoryDetailsModal
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
date={date}
|
||||||
data={data}
|
data={data}
|
||||||
indexer={indexer}
|
indexer={indexer}
|
||||||
isMarkingAsFailed={isMarkingAsFailed}
|
isMarkingAsFailed={isMarkingAsFailed}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
|
||||||
import styles from './AddIndexerModal.css';
|
|
||||||
|
|
||||||
function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.EXTRA_LARGE}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
className={styles.modal}
|
|
||||||
>
|
|
||||||
<AddIndexerModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
onSelectIndexer={onSelectIndexer}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddIndexerModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onSelectIndexer: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddIndexerModal;
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { clearIndexerSchema } from 'Store/Actions/indexerActions';
|
||||||
|
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||||
|
import styles from './AddIndexerModal.css';
|
||||||
|
|
||||||
|
interface AddIndexerModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onSelectIndexer(): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddIndexerModal({
|
||||||
|
isOpen,
|
||||||
|
onSelectIndexer,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: AddIndexerModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onModalClosePress = useCallback(() => {
|
||||||
|
dispatch(clearIndexerSchema());
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
|
className={styles.modal}
|
||||||
|
onModalClose={onModalClosePress}
|
||||||
|
>
|
||||||
|
<AddIndexerModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onSelectIndexer={onSelectIndexer}
|
||||||
|
onModalClose={onModalClosePress}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddIndexerModal;
|
||||||
@@ -19,12 +19,18 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.notice {
|
||||||
composes: alert from '~Components/Alert.css';
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.scroller {
|
.scroller {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface CssExports {
|
|||||||
'indexers': string;
|
'indexers': string;
|
||||||
'modalBody': string;
|
'modalBody': string;
|
||||||
'modalFooter': string;
|
'modalFooter': string;
|
||||||
|
'notice': string;
|
||||||
'scroller': string;
|
'scroller': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
|
||||||
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
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 Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import SelectIndexerRow from './SelectIndexerRow';
|
|
||||||
import styles from './AddIndexerModalContent.css';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'protocol',
|
|
||||||
label: () => translate('Protocol'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sortName',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'language',
|
|
||||||
label: () => translate('Language'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
label: () => translate('Description'),
|
|
||||||
isSortable: false,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'privacy',
|
|
||||||
label: () => translate('Privacy'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'categories',
|
|
||||||
label: () => translate('Categories'),
|
|
||||||
isSortable: false,
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const protocols = [
|
|
||||||
{
|
|
||||||
key: 'torrent',
|
|
||||||
value: 'torrent'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'usenet',
|
|
||||||
value: 'nzb'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const privacyLevels = [
|
|
||||||
{
|
|
||||||
key: 'private',
|
|
||||||
get value() {
|
|
||||||
return translate('Private');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'semiPrivate',
|
|
||||||
get value() {
|
|
||||||
return translate('SemiPrivate');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'public',
|
|
||||||
get value() {
|
|
||||||
return translate('Public');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class AddIndexerModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
filter: '',
|
|
||||||
filterProtocols: [],
|
|
||||||
filterLanguages: [],
|
|
||||||
filterPrivacyLevels: [],
|
|
||||||
filterCategories: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFilterChange = ({ value }) => {
|
|
||||||
this.setState({ filter: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
indexers,
|
|
||||||
onIndexerSelect,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
onSortPress,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const languages = Array.from(new Set(indexers.map(({ language }) => language)))
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
.map((language) => ({ key: language, value: language }));
|
|
||||||
|
|
||||||
const filteredIndexers = indexers.filter((indexer) => {
|
|
||||||
const {
|
|
||||||
filter,
|
|
||||||
filterProtocols,
|
|
||||||
filterLanguages,
|
|
||||||
filterPrivacyLevels,
|
|
||||||
filterCategories
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterLanguages.length && !filterLanguages.includes(indexer.language)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterCategories.length) {
|
|
||||||
const { categories = [] } = indexer.capabilities || {};
|
|
||||||
const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
|
|
||||||
const flatCategories = categories
|
|
||||||
.filter((item) => item.id < 100000)
|
|
||||||
.flatMap(flat);
|
|
||||||
|
|
||||||
if (!filterCategories.every((item) => flatCategories.includes(item))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AddIndexer')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody
|
|
||||||
className={styles.modalBody}
|
|
||||||
scrollDirection={scrollDirections.NONE}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
className={styles.filterInput}
|
|
||||||
placeholder={translate('FilterPlaceHolder')}
|
|
||||||
name="filter"
|
|
||||||
value={this.state.filter}
|
|
||||||
autoFocus={true}
|
|
||||||
onChange={this.onFilterChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.filterRow}>
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Protocol')}</label>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
name="indexerProtocols"
|
|
||||||
value={this.state.filterProtocols}
|
|
||||||
values={protocols}
|
|
||||||
onChange={({ value }) => this.setState({ filterProtocols: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Language')}</label>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
name="indexerLanguages"
|
|
||||||
value={this.state.filterLanguages}
|
|
||||||
values={languages}
|
|
||||||
onChange={({ value }) => this.setState({ filterLanguages: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
name="indexerPrivacyLevels"
|
|
||||||
value={this.state.filterPrivacyLevels}
|
|
||||||
values={privacyLevels}
|
|
||||||
onChange={({ value }) => this.setState({ filterPrivacyLevels: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Categories')}</label>
|
|
||||||
<NewznabCategorySelectInputConnector
|
|
||||||
name="indexerCategories"
|
|
||||||
value={this.state.filterCategories}
|
|
||||||
onChange={({ value }) => this.setState({ filterCategories: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
kind={kinds.INFO}
|
|
||||||
className={styles.alert}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{translate('ProwlarrSupportsAnyIndexer')}
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Scroller
|
|
||||||
className={styles.scroller}
|
|
||||||
autoFocus={false}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isFetching ? <LoadingIndicator /> : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
error ? <Alert kind={kinds.DANGER}>{errorMessage}</Alert> : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
isPopulated && !!indexers.length ?
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
filteredIndexers.map((indexer) => (
|
|
||||||
<SelectIndexerRow
|
|
||||||
key={`${indexer.implementation}-${indexer.name}`}
|
|
||||||
implementation={indexer.implementation}
|
|
||||||
implementationName={indexer.implementationName}
|
|
||||||
{...indexer}
|
|
||||||
onIndexerSelect={onIndexerSelect}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
isPopulated && !!indexers.length && !filteredIndexers.length ?
|
|
||||||
<Alert
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
{translate('NoIndexersFound')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</Scroller>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
|
||||||
<div className={styles.available}>
|
|
||||||
{
|
|
||||||
isPopulated ?
|
|
||||||
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
|
||||||
</div>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddIndexerModalContent.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.string,
|
|
||||||
onSortPress: PropTypes.func.isRequired,
|
|
||||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onIndexerSelect: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddIndexerModalContent;
|
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
import { some } from 'lodash';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import IndexerAppState from 'App/State/IndexerAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
||||||
|
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
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 Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||||
|
import Indexer, { IndexerCategory } from 'Indexer/Indexer';
|
||||||
|
import {
|
||||||
|
fetchIndexerSchema,
|
||||||
|
selectIndexerSchema,
|
||||||
|
setIndexerSchemaSort,
|
||||||
|
} from 'Store/Actions/indexerActions';
|
||||||
|
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import { SortCallback } from 'typings/callbacks';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import SelectIndexerRow from './SelectIndexerRow';
|
||||||
|
import styles from './AddIndexerModalContent.css';
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{
|
||||||
|
name: 'protocol',
|
||||||
|
label: () => translate('Protocol'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortName',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'language',
|
||||||
|
label: () => translate('Language'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
label: () => translate('Description'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'privacy',
|
||||||
|
label: () => translate('Privacy'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categories',
|
||||||
|
label: () => translate('Categories'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROTOCOLS = [
|
||||||
|
{
|
||||||
|
key: 'torrent',
|
||||||
|
value: 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'usenet',
|
||||||
|
value: 'nzb',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRIVACY_LEVELS = [
|
||||||
|
{
|
||||||
|
key: 'private',
|
||||||
|
get value() {
|
||||||
|
return translate('Private');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'semiPrivate',
|
||||||
|
get value() {
|
||||||
|
return translate('SemiPrivate');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'public',
|
||||||
|
get value() {
|
||||||
|
return translate('Public');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface IndexerSchema extends Indexer {
|
||||||
|
isExistingIndexer: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAddIndexersSelector() {
|
||||||
|
return createSelector(
|
||||||
|
createClientSideCollectionSelector('indexers.schema'),
|
||||||
|
createAllIndexersSelector(),
|
||||||
|
(indexers: IndexerAppState, allIndexers) => {
|
||||||
|
const { isFetching, isPopulated, error, items, sortDirection, sortKey } =
|
||||||
|
indexers;
|
||||||
|
|
||||||
|
const indexerList: IndexerSchema[] = items.map((item) => {
|
||||||
|
const { definitionName } = item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
isExistingIndexer: some(allIndexers, { definitionName }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
indexers: indexerList,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddIndexerModalContentProps {
|
||||||
|
onSelectIndexer(): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddIndexerModalContent(props: AddIndexerModalContentProps) {
|
||||||
|
const { onSelectIndexer, onModalClose } = props;
|
||||||
|
|
||||||
|
const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } =
|
||||||
|
useSelector(createAddIndexersSelector());
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
const [filterProtocols, setFilterProtocols] = useState<string[]>([]);
|
||||||
|
const [filterLanguages, setFilterLanguages] = useState<string[]>([]);
|
||||||
|
const [filterPrivacyLevels, setFilterPrivacyLevels] = useState<string[]>([]);
|
||||||
|
const [filterCategories, setFilterCategories] = useState<number[]>([]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
dispatch(fetchIndexerSchema());
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
setFilter(value);
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterProtocolsChange = useCallback(
|
||||||
|
({ value }: { value: string[] }) => {
|
||||||
|
setFilterProtocols(value);
|
||||||
|
},
|
||||||
|
[setFilterProtocols]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterLanguagesChange = useCallback(
|
||||||
|
({ value }: { value: string[] }) => {
|
||||||
|
setFilterLanguages(value);
|
||||||
|
},
|
||||||
|
[setFilterLanguages]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterPrivacyLevelsChange = useCallback(
|
||||||
|
({ value }: { value: string[] }) => {
|
||||||
|
setFilterPrivacyLevels(value);
|
||||||
|
},
|
||||||
|
[setFilterPrivacyLevels]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterCategoriesChange = useCallback(
|
||||||
|
({ value }: { value: number[] }) => {
|
||||||
|
setFilterCategories(value);
|
||||||
|
},
|
||||||
|
[setFilterCategories]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onIndexerSelect = useCallback(
|
||||||
|
({
|
||||||
|
implementation,
|
||||||
|
implementationName,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
implementation: string;
|
||||||
|
implementationName: string;
|
||||||
|
name: string;
|
||||||
|
}) => {
|
||||||
|
dispatch(
|
||||||
|
selectIndexerSchema({
|
||||||
|
implementation,
|
||||||
|
implementationName,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
onSelectIndexer();
|
||||||
|
},
|
||||||
|
[dispatch, onSelectIndexer]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSortPress = useCallback<SortCallback>(
|
||||||
|
(sortKey, sortDirection) => {
|
||||||
|
dispatch(setIndexerSchemaSort({ sortKey, sortDirection }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const languages = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from(new Set(indexers.map(({ language }) => language)))
|
||||||
|
.map((language) => ({ key: language, value: language }))
|
||||||
|
.sort(sortByProp('value')),
|
||||||
|
[indexers]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredIndexers = useMemo(() => {
|
||||||
|
const flat = ({
|
||||||
|
id,
|
||||||
|
subCategories = [],
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
subCategories: IndexerCategory[];
|
||||||
|
}): number[] => [id, ...subCategories.flatMap(flat)];
|
||||||
|
|
||||||
|
return indexers.filter((indexer) => {
|
||||||
|
if (
|
||||||
|
filter.length &&
|
||||||
|
!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) &&
|
||||||
|
!indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterProtocols.length &&
|
||||||
|
!filterProtocols.includes(indexer.protocol)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterLanguages.length &&
|
||||||
|
!filterLanguages.includes(indexer.language)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterPrivacyLevels.length &&
|
||||||
|
!filterPrivacyLevels.includes(indexer.privacy)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterCategories.length) {
|
||||||
|
const { categories = [] } = indexer.capabilities || {};
|
||||||
|
|
||||||
|
const flatCategories = categories
|
||||||
|
.filter((item) => item.id < 100000)
|
||||||
|
.flatMap(flat);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!filterCategories.every((categoryId) =>
|
||||||
|
flatCategories.includes(categoryId)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
indexers,
|
||||||
|
filter,
|
||||||
|
filterProtocols,
|
||||||
|
filterLanguages,
|
||||||
|
filterPrivacyLevels,
|
||||||
|
filterCategories,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const errorMessage = getErrorMessage(
|
||||||
|
error,
|
||||||
|
translate('UnableToLoadIndexers')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody
|
||||||
|
className={styles.modalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
className={styles.filterInput}
|
||||||
|
placeholder={translate('FilterPlaceHolder')}
|
||||||
|
name="filter"
|
||||||
|
value={filter}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={onFilterChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.filterRow}>
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>
|
||||||
|
{translate('Protocol')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name="indexerProtocols"
|
||||||
|
value={filterProtocols}
|
||||||
|
values={PROTOCOLS}
|
||||||
|
onChange={onFilterProtocolsChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>
|
||||||
|
{translate('Language')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name="indexerLanguages"
|
||||||
|
value={filterLanguages}
|
||||||
|
values={languages}
|
||||||
|
onChange={onFilterLanguagesChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name="indexerPrivacyLevels"
|
||||||
|
value={filterPrivacyLevels}
|
||||||
|
values={PRIVACY_LEVELS}
|
||||||
|
onChange={onFilterPrivacyLevelsChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>
|
||||||
|
{translate('Categories')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<NewznabCategorySelectInputConnector
|
||||||
|
name="indexerCategories"
|
||||||
|
value={filterCategories}
|
||||||
|
onChange={onFilterCategoriesChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert kind={kinds.INFO} className={styles.notice}>
|
||||||
|
<div>{translate('ProwlarrSupportsAnyIndexer')}</div>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Scroller className={styles.scroller} autoFocus={false}>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !!indexers.length ? (
|
||||||
|
<Table
|
||||||
|
columns={COLUMNS}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{filteredIndexers.map((indexer) => (
|
||||||
|
<SelectIndexerRow
|
||||||
|
{...indexer}
|
||||||
|
key={`${indexer.implementation}-${indexer.name}`}
|
||||||
|
implementation={indexer.implementation}
|
||||||
|
implementationName={indexer.implementationName}
|
||||||
|
onIndexerSelect={onIndexerSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !!indexers.length && !filteredIndexers.length ? (
|
||||||
|
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||||
|
{translate('NoIndexersFound')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
</Scroller>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div className={styles.available}>
|
||||||
|
{isPopulated
|
||||||
|
? translate('CountIndexersAvailable', {
|
||||||
|
count: filteredIndexers.length,
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddIndexerModalContent;
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { some } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
|
|
||||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createClientSideCollectionSelector('indexers.schema'),
|
|
||||||
createAllIndexersSelector(),
|
|
||||||
(indexers, allIndexers) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
sortDirection,
|
|
||||||
sortKey
|
|
||||||
} = indexers;
|
|
||||||
|
|
||||||
const indexerList = items.map((item) => {
|
|
||||||
const { definitionName } = item;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
isExistingIndexer: some(allIndexers, { definitionName })
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
indexers: indexerList,
|
|
||||||
sortKey,
|
|
||||||
sortDirection
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchIndexerSchema,
|
|
||||||
selectIndexerSchema,
|
|
||||||
setIndexerSchemaSort
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddIndexerModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchIndexerSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onIndexerSelect = ({ implementation, implementationName, name }) => {
|
|
||||||
this.props.selectIndexerSchema({ implementation, implementationName, name });
|
|
||||||
this.props.onSelectIndexer();
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey, sortDirection) => {
|
|
||||||
this.props.setIndexerSchemaSort({ sortKey, sortDirection });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AddIndexerModalContent
|
|
||||||
{...this.props}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onIndexerSelect={this.onIndexerSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddIndexerModalContentConnector.propTypes = {
|
|
||||||
fetchIndexerSchema: PropTypes.func.isRequired,
|
|
||||||
selectIndexerSchema: PropTypes.func.isRequired,
|
|
||||||
setIndexerSchemaSort: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onSelectIndexer: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
|
|
||||||
@@ -2,18 +2,19 @@ import React, { useCallback } from 'react';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRowButton from 'Components/Table/TableRowButton';
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||||
|
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer';
|
||||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './SelectIndexerRow.css';
|
import styles from './SelectIndexerRow.css';
|
||||||
|
|
||||||
interface SelectIndexerRowProps {
|
interface SelectIndexerRowProps {
|
||||||
name: string;
|
name: string;
|
||||||
protocol: string;
|
protocol: DownloadProtocol;
|
||||||
privacy: string;
|
privacy: IndexerPrivacy;
|
||||||
language: string;
|
language: string;
|
||||||
description: string;
|
description: string;
|
||||||
capabilities: IndexerCapabilities;
|
capabilities: IndexerCapabilities;
|
||||||
@@ -63,7 +64,9 @@ function SelectIndexerRow(props: SelectIndexerRowProps) {
|
|||||||
|
|
||||||
<TableRowCell>{description}</TableRowCell>
|
<TableRowCell>{description}</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>{translate(firstCharToUpper(privacy))}</TableRowCell>
|
<TableRowCell>
|
||||||
|
<PrivacyLabel privacy={privacy} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
<CapabilitiesLabel capabilities={capabilities} />
|
<CapabilitiesLabel capabilities={capabilities} />
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { SelectProvider } from 'App/SelectContext';
|
import { SelectProvider } from 'App/SelectContext';
|
||||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||||
@@ -22,12 +28,17 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
|||||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||||
import NoIndexer from 'Indexer/NoIndexer';
|
import NoIndexer from 'Indexer/NoIndexer';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { cloneIndexer, testAllIndexers } from 'Store/Actions/indexerActions';
|
import {
|
||||||
|
cloneIndexer,
|
||||||
|
fetchIndexers,
|
||||||
|
testAllIndexers,
|
||||||
|
} from 'Store/Actions/indexerActions';
|
||||||
import {
|
import {
|
||||||
setIndexerFilter,
|
setIndexerFilter,
|
||||||
setIndexerSort,
|
setIndexerSort,
|
||||||
setIndexerTableOption,
|
setIndexerTableOption,
|
||||||
} from 'Store/Actions/indexerIndexActions';
|
} from 'Store/Actions/indexerIndexActions';
|
||||||
|
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
@@ -82,6 +93,11 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
|||||||
);
|
);
|
||||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchIndexers());
|
||||||
|
dispatch(fetchIndexerStatus());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const onAddIndexerPress = useCallback(() => {
|
const onAddIndexerPress = useCallback(() => {
|
||||||
setIsAddIndexerModalOpen(true);
|
setIsAddIndexerModalOpen(true);
|
||||||
}, [setIsAddIndexerModalOpen]);
|
}, [setIsAddIndexerModalOpen]);
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
name="seedRatio"
|
name="seedRatio"
|
||||||
value={seedRatio}
|
value={seedRatio}
|
||||||
helpText={translate('SeedRatioHelpText')}
|
helpText={translate('SeedRatioHelpText')}
|
||||||
|
isFloat={true}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { uniqBy } from 'lodash';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
import { IndexerCapabilities } from 'Indexer/Indexer';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
interface CapabilitiesLabelProps {
|
interface CapabilitiesLabelProps {
|
||||||
capabilities: IndexerCapabilities;
|
capabilities: IndexerCapabilities;
|
||||||
@@ -38,7 +39,7 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{filteredList.length === 0 ? <Label>{'None'}</Label> : null}
|
{filteredList.length === 0 ? <Label>{translate('None')}</Label> : null}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
@@ -15,10 +14,10 @@ import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItem
|
|||||||
import Indexer from 'Indexer/Indexer';
|
import Indexer from 'Indexer/Indexer';
|
||||||
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
|
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CapabilitiesLabel from './CapabilitiesLabel';
|
import CapabilitiesLabel from './CapabilitiesLabel';
|
||||||
import IndexerStatusCell from './IndexerStatusCell';
|
import IndexerStatusCell from './IndexerStatusCell';
|
||||||
|
import PrivacyLabel from './PrivacyLabel';
|
||||||
import ProtocolLabel from './ProtocolLabel';
|
import ProtocolLabel from './ProtocolLabel';
|
||||||
import styles from './IndexerIndexRow.css';
|
import styles from './IndexerIndexRow.css';
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
|||||||
if (name === 'privacy') {
|
if (name === 'privacy') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
<Label>{translate(firstCharToUpper(privacy))}</Label>
|
<PrivacyLabel privacy={privacy} />
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,11 +46,7 @@ const columnsSelector = createSelector(
|
|||||||
(columns) => columns
|
(columns) => columns
|
||||||
);
|
);
|
||||||
|
|
||||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||||
index,
|
|
||||||
style,
|
|
||||||
data,
|
|
||||||
}) => {
|
|
||||||
const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data;
|
const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data;
|
||||||
|
|
||||||
if (index >= items.length) {
|
if (index >= items.length) {
|
||||||
@@ -77,7 +73,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function getWindowScrollTopPosition() {
|
function getWindowScrollTopPosition() {
|
||||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Fragment, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
@@ -32,19 +32,17 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<FormGroup>
|
||||||
<FormGroup>
|
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showSearchAction"
|
name="showSearchAction"
|
||||||
value={showSearchAction}
|
value={showSearchAction}
|
||||||
helpText={translate('ShowSearchHelpText')}
|
helpText={translate('ShowSearchHelpText')}
|
||||||
onChange={onTableOptionChangeWrapper}
|
onChange={onTableOptionChangeWrapper}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,30 @@ import translate from 'Utilities/String/translate';
|
|||||||
import DisabledIndexerInfo from './DisabledIndexerInfo';
|
import DisabledIndexerInfo from './DisabledIndexerInfo';
|
||||||
import styles from './IndexerStatusCell.css';
|
import styles from './IndexerStatusCell.css';
|
||||||
|
|
||||||
|
function getIconKind(enabled: boolean, redirect: boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
return redirect ? kinds.INFO : kinds.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinds.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconName(enabled: boolean, redirect: boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
return redirect ? icons.REDIRECT : icons.CHECK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons.BLOCKLIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconTooltip(enabled: boolean, redirect: boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
return redirect ? translate('EnabledRedirected') : translate('Enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('Disabled');
|
||||||
|
}
|
||||||
|
|
||||||
interface IndexerStatusCellProps {
|
interface IndexerStatusCellProps {
|
||||||
className: string;
|
className: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -30,22 +54,14 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
|
|
||||||
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
|
|
||||||
const enableTitle = redirect
|
|
||||||
? translate('EnabledRedirected')
|
|
||||||
: translate('Enabled');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component className={className} {...otherProps}>
|
<Component className={className} {...otherProps}>
|
||||||
{
|
<Icon
|
||||||
<Icon
|
className={styles.statusIcon}
|
||||||
className={styles.statusIcon}
|
kind={getIconKind(enabled, redirect)}
|
||||||
kind={enabled ? enableKind : kinds.DEFAULT}
|
name={getIconName(enabled, redirect)}
|
||||||
name={enabled ? enableIcon : icons.BLOCKLIST}
|
title={getIconTooltip(enabled, redirect)}
|
||||||
title={enabled ? enableTitle : translate('Disabled')}
|
/>
|
||||||
/>
|
|
||||||
}
|
|
||||||
{status ? (
|
{status ? (
|
||||||
<Popover
|
<Popover
|
||||||
className={styles.indexerStatusTooltip}
|
className={styles.indexerStatusTooltip}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.publicLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
border-color: var(--dangerColor);
|
||||||
|
background-color: var(--dangerColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.semiPrivateLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
border-color: var(--warningColor);
|
||||||
|
background-color: var(--warningColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.privateLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
border-color: var(--infoColor);
|
||||||
|
background-color: var(--infoColor);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'privateLabel': string;
|
||||||
|
'publicLabel': string;
|
||||||
|
'semiPrivateLabel': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import { IndexerPrivacy } from 'Indexer/Indexer';
|
||||||
|
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './PrivacyLabel.css';
|
||||||
|
|
||||||
|
interface PrivacyLabelProps {
|
||||||
|
privacy: IndexerPrivacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrivacyLabel({ privacy }: PrivacyLabelProps) {
|
||||||
|
return (
|
||||||
|
<Label className={styles[`${privacy}Label`]}>
|
||||||
|
{translate(firstCharToUpper(privacy))}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrivacyLabel;
|
||||||
@@ -11,3 +11,7 @@
|
|||||||
border-color: var(--usenetColor);
|
border-color: var(--usenetColor);
|
||||||
background-color: var(--usenetColor);
|
background-color: var(--usenetColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unknown {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'torrent': string;
|
'torrent': string;
|
||||||
|
'unknown': string;
|
||||||
'usenet': string;
|
'usenet': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
import styles from './ProtocolLabel.css';
|
import styles from './ProtocolLabel.css';
|
||||||
|
|
||||||
interface ProtocolLabelProps {
|
interface ProtocolLabelProps {
|
||||||
protocol: string;
|
protocol: DownloadProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProtocolLabel(props: ProtocolLabelProps) {
|
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
|
||||||
const { protocol } = props;
|
|
||||||
|
|
||||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore ts(7053)
|
|
||||||
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
|
||||||
export interface IndexerStatus extends ModelBase {
|
export interface IndexerStatus extends ModelBase {
|
||||||
disabledTill: Date;
|
disabledTill: Date;
|
||||||
@@ -24,6 +25,8 @@ export interface IndexerCapabilities extends ModelBase {
|
|||||||
categories: IndexerCategory[];
|
categories: IndexerCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IndexerPrivacy = 'public' | 'semiPrivate' | 'private';
|
||||||
|
|
||||||
export interface IndexerField extends ModelBase {
|
export interface IndexerField extends ModelBase {
|
||||||
order: number;
|
order: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -36,6 +39,7 @@ export interface IndexerField extends ModelBase {
|
|||||||
|
|
||||||
interface Indexer extends ModelBase {
|
interface Indexer extends ModelBase {
|
||||||
name: string;
|
name: string;
|
||||||
|
definitionName: string;
|
||||||
description: string;
|
description: string;
|
||||||
encoding: string;
|
encoding: string;
|
||||||
language: string;
|
language: string;
|
||||||
@@ -46,8 +50,8 @@ interface Indexer extends ModelBase {
|
|||||||
supportsSearch: boolean;
|
supportsSearch: boolean;
|
||||||
supportsRedirect: boolean;
|
supportsRedirect: boolean;
|
||||||
supportsPagination: boolean;
|
supportsPagination: boolean;
|
||||||
protocol: string;
|
protocol: DownloadProtocol;
|
||||||
privacy: string;
|
privacy: IndexerPrivacy;
|
||||||
priority: number;
|
priority: number;
|
||||||
fields: IndexerField[];
|
fields: IndexerField[];
|
||||||
tags: number[];
|
tags: number[];
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) {
|
|||||||
</div>
|
</div>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCell date={date} />
|
<RelativeDateCell date={date} includeSeconds={true} />
|
||||||
|
|
||||||
<TableRowCell className={styles.source}>
|
<TableRowCell className={styles.source}>
|
||||||
{data.source ? data.source : null}
|
{data.source ? data.source : null}
|
||||||
@@ -83,14 +83,15 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) {
|
|||||||
<TableRowCell className={styles.details}>
|
<TableRowCell className={styles.details}>
|
||||||
<IconButton
|
<IconButton
|
||||||
name={icons.INFO}
|
name={icons.INFO}
|
||||||
onPress={onDetailsModalPress}
|
|
||||||
title={translate('HistoryDetails')}
|
title={translate('HistoryDetails')}
|
||||||
|
onPress={onDetailsModalPress}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<HistoryDetailsModal
|
<HistoryDetailsModal
|
||||||
isOpen={isDetailsModalOpen}
|
isOpen={isDetailsModalOpen}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
date={date}
|
||||||
data={data}
|
data={data}
|
||||||
indexer={indexer}
|
indexer={indexer}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import TagListConnector from 'Components/TagListConnector';
|
|||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||||
|
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||||
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
|
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
|
||||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -64,6 +65,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
|||||||
fields,
|
fields,
|
||||||
tags,
|
tags,
|
||||||
protocol,
|
protocol,
|
||||||
|
privacy,
|
||||||
capabilities = {} as IndexerCapabilities,
|
capabilities = {} as IndexerCapabilities,
|
||||||
} = indexer as Indexer;
|
} = indexer as Indexer;
|
||||||
|
|
||||||
@@ -160,6 +162,11 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
|||||||
title={translate('Language')}
|
title={translate('Language')}
|
||||||
data={language ?? '-'}
|
data={language ?? '-'}
|
||||||
/>
|
/>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Privacy')}
|
||||||
|
data={privacy ? <PrivacyLabel privacy={privacy} /> : '-'}
|
||||||
|
/>
|
||||||
{vipExpiration ? (
|
{vipExpiration ? (
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
.message {
|
.message {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -14,11 +15,9 @@ function NoIndexer(props: NoIndexerProps) {
|
|||||||
|
|
||||||
if (totalItems > 0) {
|
if (totalItems > 0) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Alert kind={kinds.WARNING} className={styles.message}>
|
||||||
<div className={styles.message}>
|
{translate('AllIndexersHiddenDueToFilter')}
|
||||||
{translate('AllIndexersHiddenDueToFilter')}
|
</Alert>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ function NoIndexer(props: NoIndexerProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<Button onPress={onAddIndexerPress} kind={kinds.PRIMARY}>
|
<Button kind={kinds.PRIMARY} onPress={onAddIndexerPress}>
|
||||||
{translate('AddNewIndexer')}
|
{translate('AddNewIndexer')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,23 +32,30 @@ import IndexerStatsFilterModal from './IndexerStatsFilterModal';
|
|||||||
import styles from './IndexerStats.css';
|
import styles from './IndexerStats.css';
|
||||||
|
|
||||||
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
|
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
|
||||||
const data = indexerStats.map((indexer) => {
|
const statistics = [...indexerStats].sort((a, b) =>
|
||||||
return {
|
a.averageResponseTime === b.averageResponseTime
|
||||||
label: indexer.indexerName,
|
? b.averageGrabResponseTime - a.averageGrabResponseTime
|
||||||
value: indexer.averageResponseTime,
|
: b.averageResponseTime - a.averageResponseTime
|
||||||
};
|
);
|
||||||
});
|
|
||||||
|
|
||||||
data.sort((a, b) => {
|
return {
|
||||||
return b.value - a.value;
|
labels: statistics.map((indexer) => indexer.indexerName),
|
||||||
});
|
datasets: [
|
||||||
|
{
|
||||||
return data;
|
label: translate('AverageQueries'),
|
||||||
|
data: statistics.map((indexer) => indexer.averageResponseTime),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: translate('AverageGrabs'),
|
||||||
|
data: statistics.map((indexer) => indexer.averageGrabResponseTime),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
|
function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
|
||||||
const data = indexerStats.map((indexer) => {
|
const data = [...indexerStats]
|
||||||
return {
|
.map((indexer) => ({
|
||||||
label: indexer.indexerName,
|
label: indexer.indexerName,
|
||||||
value:
|
value:
|
||||||
(indexer.numberOfFailedQueries +
|
(indexer.numberOfFailedQueries +
|
||||||
@@ -59,109 +66,102 @@ function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
|
|||||||
indexer.numberOfRssQueries +
|
indexer.numberOfRssQueries +
|
||||||
indexer.numberOfAuthQueries +
|
indexer.numberOfAuthQueries +
|
||||||
indexer.numberOfGrabs),
|
indexer.numberOfGrabs),
|
||||||
};
|
}))
|
||||||
});
|
.filter((s) => s.value > 0);
|
||||||
|
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => b.value - a.value);
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
|
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
|
||||||
const data = {
|
const statistics = [...indexerStats]
|
||||||
labels: indexerStats.map((indexer) => indexer.indexerName),
|
.filter(
|
||||||
|
(s) =>
|
||||||
|
s.numberOfQueries > 0 ||
|
||||||
|
s.numberOfRssQueries > 0 ||
|
||||||
|
s.numberOfAuthQueries > 0
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.numberOfQueries +
|
||||||
|
b.numberOfRssQueries +
|
||||||
|
b.numberOfAuthQueries -
|
||||||
|
(a.numberOfQueries + a.numberOfRssQueries + a.numberOfAuthQueries)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: statistics.map((indexer) => indexer.indexerName),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: translate('SearchQueries'),
|
label: translate('SearchQueries'),
|
||||||
data: indexerStats.map((indexer) => indexer.numberOfQueries),
|
data: statistics.map((indexer) => indexer.numberOfQueries),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('RssQueries'),
|
label: translate('RssQueries'),
|
||||||
data: indexerStats.map((indexer) => indexer.numberOfRssQueries),
|
data: statistics.map((indexer) => indexer.numberOfRssQueries),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: translate('AuthQueries'),
|
label: translate('AuthQueries'),
|
||||||
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries),
|
data: statistics.map((indexer) => indexer.numberOfAuthQueries),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
|
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
|
||||||
const data = indexerStats.map((indexer) => {
|
const data = [...indexerStats]
|
||||||
return {
|
.map((indexer) => ({
|
||||||
label: indexer.indexerName,
|
label: indexer.indexerName,
|
||||||
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
|
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
|
||||||
};
|
}))
|
||||||
});
|
.filter((s) => s.value > 0);
|
||||||
|
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => b.value - a.value);
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) {
|
function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) {
|
||||||
const data = indexerStats.map((indexer) => {
|
const data = indexerStats.map((indexer) => ({
|
||||||
return {
|
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
||||||
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
value: indexer.numberOfGrabs,
|
||||||
value: indexer.numberOfGrabs,
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => b.value - a.value);
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) {
|
function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) {
|
||||||
const data = indexerStats.map((indexer) => {
|
const data = indexerStats.map((indexer) => ({
|
||||||
return {
|
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
||||||
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
value: indexer.numberOfQueries,
|
||||||
value: indexer.numberOfQueries,
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => b.value - a.value);
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHostGrabsData(indexerStats: IndexerStatsHost[]) {
|
function getHostGrabsData(indexerStats: IndexerStatsHost[]) {
|
||||||
const data = indexerStats.map((indexer) => {
|
const data = indexerStats.map((indexer) => ({
|
||||||
return {
|
label: indexer.host ? indexer.host : 'Other',
|
||||||
label: indexer.host ? indexer.host : 'Other',
|
value: indexer.numberOfGrabs,
|
||||||
value: indexer.numberOfGrabs,
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => b.value - a.value);
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHostQueryData(indexerStats: IndexerStatsHost[]) {
|
function getHostQueryData(indexerStats: IndexerStatsHost[]) {
|
||||||
const data = indexerStats.map((indexer) => {
|
const data = indexerStats.map((indexer) => ({
|
||||||
return {
|
label: indexer.host ? indexer.host : 'Other',
|
||||||
label: indexer.host ? indexer.host : 'Other',
|
value: indexer.numberOfQueries,
|
||||||
value: indexer.numberOfQueries,
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => b.value - a.value);
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -241,9 +241,9 @@ function IndexerStats() {
|
|||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={customFilters}
|
customFilters={customFilters}
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
filterModalConnectorComponent={IndexerStatsFilterModal}
|
filterModalConnectorComponent={IndexerStatsFilterModal}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
@@ -294,7 +294,7 @@ function IndexerStats() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.fullWidthChart}>
|
<div className={styles.fullWidthChart}>
|
||||||
<div className={styles.chartContainer}>
|
<div className={styles.chartContainer}>
|
||||||
<BarChart
|
<StackedBarChart
|
||||||
data={getAverageResponseTimeData(item.indexers)}
|
data={getAverageResponseTimeData(item.indexers)}
|
||||||
title={translate('AverageResponseTimesMs')}
|
title={translate('AverageResponseTimesMs')}
|
||||||
stepSize={100}
|
stepSize={100}
|
||||||
|
|||||||
@@ -47,3 +47,42 @@ $hoverScale: 1.05;
|
|||||||
right: 0;
|
right: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloadLink {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
color: var(--textColor);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualDownloadContent {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
height: 20.39px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 20.39px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--iconButtonHoverColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactiveIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 0;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 8px;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ interface CssExports {
|
|||||||
'actions': string;
|
'actions': string;
|
||||||
'container': string;
|
'container': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
|
'downloadIcon': string;
|
||||||
|
'downloadLink': string;
|
||||||
'indexerRow': string;
|
'indexerRow': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'infoRow': string;
|
'infoRow': string;
|
||||||
|
'interactiveIcon': string;
|
||||||
|
'manualDownloadContent': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
'titleRow': string;
|
'titleRow': string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TextTruncate from 'react-text-truncate';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
|
||||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
|
||||||
import Peers from 'Search/Table/Peers';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './SearchIndexOverview.css';
|
|
||||||
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
|
|
||||||
function getContentHeight(rowHeight, isSmallScreen) {
|
|
||||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
|
||||||
|
|
||||||
return rowHeight - padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return icons.SPINNER;
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
} else if (grabError) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons.DOWNLOAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadKind(isGrabbed, grabError) {
|
|
||||||
if (isGrabbed) {
|
|
||||||
return kinds.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grabError) {
|
|
||||||
return kinds.DANGER;
|
|
||||||
}
|
|
||||||
|
|
||||||
return kinds.DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return '';
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return translate('AddedToDownloadClient');
|
|
||||||
} else if (grabError) {
|
|
||||||
return grabError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate('AddToDownloadClient');
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchIndexOverview extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onGrabPress({
|
|
||||||
guid,
|
|
||||||
indexerId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
infoUrl,
|
|
||||||
protocol,
|
|
||||||
downloadUrl,
|
|
||||||
magnetUrl,
|
|
||||||
categories,
|
|
||||||
seeders,
|
|
||||||
leechers,
|
|
||||||
indexerFlags,
|
|
||||||
size,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
indexer,
|
|
||||||
rowHeight,
|
|
||||||
isSmallScreen,
|
|
||||||
isGrabbed,
|
|
||||||
isGrabbing,
|
|
||||||
grabError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.info} style={{ height: contentHeight }}>
|
|
||||||
<div className={styles.titleRow}>
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Link
|
|
||||||
to={infoUrl}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<TextTruncate
|
|
||||||
line={2}
|
|
||||||
text={title}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={getDownloadKind(isGrabbed, grabError)}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadUrl || magnetUrl ?
|
|
||||||
<IconButton
|
|
||||||
name={icons.SAVE}
|
|
||||||
title={translate('Save')}
|
|
||||||
to={downloadUrl ?? magnetUrl}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.indexerRow}>
|
|
||||||
{indexer}
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoRow}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatAge(age, ageHours, ageMinutes)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<CategoryLabel
|
|
||||||
categories={categories}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
indexerFlags.length ?
|
|
||||||
indexerFlags
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
.map((flag, index) => {
|
|
||||||
return (
|
|
||||||
<Label key={index} kind={kinds.INFO}>
|
|
||||||
{titleCase(flag)}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}) :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchIndexOverview.propTypes = {
|
|
||||||
guid: PropTypes.string.isRequired,
|
|
||||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
age: PropTypes.number.isRequired,
|
|
||||||
ageHours: PropTypes.number.isRequired,
|
|
||||||
ageMinutes: PropTypes.number.isRequired,
|
|
||||||
publishDate: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
infoUrl: PropTypes.string.isRequired,
|
|
||||||
downloadUrl: PropTypes.string,
|
|
||||||
magnetUrl: PropTypes.string,
|
|
||||||
indexerId: PropTypes.number.isRequired,
|
|
||||||
indexer: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
files: PropTypes.number,
|
|
||||||
grabs: PropTypes.number,
|
|
||||||
seeders: PropTypes.number,
|
|
||||||
leechers: PropTypes.number,
|
|
||||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
rowHeight: PropTypes.number.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isGrabbed: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchIndexOverview.defaultProps = {
|
|
||||||
isGrabbing: false,
|
|
||||||
isGrabbed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchIndexOverview;
|
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||||
|
import { IndexerCategory } from 'Indexer/Indexer';
|
||||||
|
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
|
||||||
|
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||||
|
import Peers from 'Search/Table/Peers';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SearchIndexOverview.css';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
function getDownloadIcon(
|
||||||
|
isGrabbing: boolean,
|
||||||
|
isGrabbed: boolean,
|
||||||
|
grabError?: string
|
||||||
|
) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return icons.SPINNER;
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
} else if (grabError) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons.DOWNLOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
|
||||||
|
if (isGrabbed) {
|
||||||
|
return kinds.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grabError) {
|
||||||
|
return kinds.DANGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinds.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadTooltip(
|
||||||
|
isGrabbing: boolean,
|
||||||
|
isGrabbed: boolean,
|
||||||
|
grabError?: string
|
||||||
|
) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return '';
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return translate('AddedToDownloadClient');
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('AddToDownloadClient');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchIndexOverviewProps {
|
||||||
|
guid: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
age: number;
|
||||||
|
ageHours: number;
|
||||||
|
ageMinutes: number;
|
||||||
|
publishDate: string;
|
||||||
|
title: string;
|
||||||
|
infoUrl: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
magnetUrl?: string;
|
||||||
|
indexerId: number;
|
||||||
|
indexer: string;
|
||||||
|
categories: IndexerCategory[];
|
||||||
|
size: number;
|
||||||
|
seeders?: number;
|
||||||
|
leechers?: number;
|
||||||
|
indexerFlags: string[];
|
||||||
|
isGrabbing: boolean;
|
||||||
|
isGrabbed: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
longDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
rowHeight: number;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
onGrabPress(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchIndexOverview(props: SearchIndexOverviewProps) {
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
protocol,
|
||||||
|
categories,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishDate,
|
||||||
|
title,
|
||||||
|
infoUrl,
|
||||||
|
downloadUrl,
|
||||||
|
magnetUrl,
|
||||||
|
indexer,
|
||||||
|
size,
|
||||||
|
seeders,
|
||||||
|
leechers,
|
||||||
|
indexerFlags = [],
|
||||||
|
isGrabbing = false,
|
||||||
|
isGrabbed = false,
|
||||||
|
grabError,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
onGrabPress,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { items: downloadClients } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGrabPressWrapper = useCallback(() => {
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
});
|
||||||
|
}, [guid, indexerId, onGrabPress]);
|
||||||
|
|
||||||
|
const onOverridePress = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(true);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const onOverrideModalClose = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(false);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const contentHeight = useMemo(() => {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}, [rowHeight, isSmallScreen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.info} style={{ height: contentHeight }}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Link to={infoUrl} title={title}>
|
||||||
|
<TextTruncate line={2} text={title} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={getDownloadKind(isGrabbed, grabError)}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPressWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<Link
|
||||||
|
className={styles.manualDownloadContent}
|
||||||
|
title={translate('OverrideAndAddToDownloadClient')}
|
||||||
|
onPress={onOverridePress}
|
||||||
|
>
|
||||||
|
<div className={styles.manualDownloadContent}>
|
||||||
|
<Icon
|
||||||
|
className={styles.interactiveIcon}
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
size={11}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
className={styles.downloadIcon}
|
||||||
|
name={icons.CIRCLE_DOWN}
|
||||||
|
size={9}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadUrl || magnetUrl ? (
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.SAVE}
|
||||||
|
title={translate('Save')}
|
||||||
|
to={downloadUrl ?? magnetUrl}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.indexerRow}>{indexer}</div>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
|
||||||
|
{protocol === 'torrent' && (
|
||||||
|
<Peers seeders={seeders} leechers={leechers} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Label>{formatBytes(size)}</Label>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<CategoryLabel categories={categories} />
|
||||||
|
|
||||||
|
{indexerFlags.length
|
||||||
|
? indexerFlags
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.localeCompare(b, undefined, { numeric: true })
|
||||||
|
)
|
||||||
|
.map((flag, index) => {
|
||||||
|
return (
|
||||||
|
<Label key={index} kind={kinds.INFO}>
|
||||||
|
{titleCase(flag)}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OverrideMatchModal
|
||||||
|
isOpen={isOverrideModalOpen}
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onOverrideModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchIndexOverview;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user