diff --git a/.devcontainer/Prowlarr.code-workspace b/.devcontainer/Prowlarr.code-workspace deleted file mode 100644 index a46158e44..000000000 --- a/.devcontainer/Prowlarr.code-workspace +++ /dev/null @@ -1,13 +0,0 @@ -// This file is used to open the backend and frontend in the same workspace, which is necessary as -// the frontend has vscode settings that are distinct from the backend -{ - "folders": [ - { - "path": ".." - }, - { - "path": "../frontend" - } - ], - "settings": {} -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 70473224d..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,19 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet -{ - "name": "Prowlarr", - "image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0", - "features": { - "ghcr.io/devcontainers/features/node:1": { - "nodeGypDependencies": true, - "version": "16", - "nvmVersion": "latest" - } - }, - "forwardPorts": [9696], - "customizations": { - "vscode": { - "extensions": ["esbenp.prettier-vscode"] - } - } -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f33a02cd1..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for more information: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates -# https://containers.dev/guide/dependabot - -version: 2 -updates: - - package-ecosystem: "devcontainers" - directory: "/" - schedule: - interval: weekly diff --git a/.github/labeler.yml b/.github/labeler.yml index 74160b634..21aacef8c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,31 +1,19 @@ 'Area: API': - - changed-files: - - any-glob-to-any-file: - - src/Prowlarr.Api.V1/**/* + - src/Prowlarr.Api.V1/**/* 'Area: Db-migration': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Datastore/Migration/* + - src/NzbDrone.Core/Datastore/Migration/* 'Area: Download Clients': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Download/Clients/**/* + - src/NzbDrone.Core/Download/Clients/**/* 'Area: Indexer': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Indexers/**/* + - src/NzbDrone.Core/Indexers/**/* 'Area: Notifications': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Notifications/**/* + - src/NzbDrone.Core/Notifications/**/* 'Area: UI': - - changed-files: - - any-glob-to-any-file: - - frontend/**/* - - package.json - - yarn.lock \ No newline at end of file + - frontend/**/* + - package.json + - yarn.lock diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml index 77c35366c..8f35f6bd6 100644 --- a/.github/workflows/label-actions.yml +++ b/.github/workflows/label-actions.yml @@ -18,6 +18,6 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/label-actions@v4 + - uses: dessant/label-actions@v3 with: process-only: 'issues, prs' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index ab2292824..857cfb4a7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,4 +9,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v4 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 1d50cb1f1..cf38066c5 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: '90' diff --git a/.gitignore b/.gitignore index 689b44415..d903078ef 100644 --- a/.gitignore +++ b/.gitignore @@ -127,7 +127,6 @@ coverage*.xml coverage*.json setup/Output/ *.~is -.mono # VS outout folders bin diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 7a36fefe1..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "esbenp.prettier-vscode", - "ms-dotnettools.csdevkit", - "ms-vscode-remote.remote-containers" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index d13f9426e..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md - "name": "Run Prowlarr", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build dotnet", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/_output/net6.0/Prowlarr", - "args": [], - "cwd": "${workspaceFolder}", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "integratedTerminal", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index b3e22f6d1..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build dotnet", - "command": "dotnet", - "type": "process", - "args": [ - "msbuild", - "-restore", - "${workspaceFolder}/src/Prowlarr.sln", - "-p:GenerateFullPaths=true", - "-p:Configuration=Debug", - "-p:Platform=Posix", - "-consoleloggerparameters:NoSummary;ForceNoAlign" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/src/Prowlarr.sln", - "-property:GenerateFullPaths=true", - "-consoleloggerparameters:NoSummary;ForceNoAlign" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/src/Prowlarr.sln" - ], - "problemMatcher": "$msCompile" - } - ] -} diff --git a/Logo/dottrace.svg b/Logo/dottrace.svg new file mode 100644 index 000000000..b879517cd --- /dev/null +++ b/Logo/dottrace.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/jetbrains.svg b/Logo/jetbrains.svg new file mode 100644 index 000000000..75d4d2177 --- /dev/null +++ b/Logo/jetbrains.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/resharper.svg b/Logo/resharper.svg new file mode 100644 index 000000000..24c987a78 --- /dev/null +++ b/Logo/resharper.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/rider.svg b/Logo/rider.svg new file mode 100644 index 000000000..82da35b0b --- /dev/null +++ b/Logo/rider.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + rider + + + + + + + + + + + + + + diff --git a/Logo/webstorm.svg b/Logo/webstorm.svg new file mode 100644 index 000000000..39ab7eb97 --- /dev/null +++ b/Logo/webstorm.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index e8c60546a..4973be54d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Prowlarr [![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop) -[![Translation status](https://translate.servarr.com/widget/servarr/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget) +[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget) [![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation/docker) ![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg) [![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers) @@ -68,16 +68,16 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## JetBrains -Thank you to [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. +Thank you to [JetBrains JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. -* [ReSharper ReSharper](http://www.jetbrains.com/resharper/) -* [WebStorm WebStorm](http://www.jetbrains.com/webstorm/) -* [Rider Rider](http://www.jetbrains.com/rider/) -* [dotTrace dotTrace](http://www.jetbrains.com/dottrace/) +- [ReSharper ReSharper](http://www.jetbrains.com/resharper/) +- [WebStorm WebStorm](http://www.jetbrains.com/webstorm/) +- [Rider Rider](http://www.jetbrains.com/rider/) +- [dotTrace dotTrace](http://www.jetbrains.com/dottrace/) ### License - [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -- Copyright 2010-2024 +- Copyright 2010-2022 Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dc667e803..1ce342be1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,18 +9,18 @@ variables: testsFolder: './_tests' yarnCacheFolder: $(Pipeline.Workspace)/.yarn nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages - majorVersion: '1.35.0' + majorVersion: '1.13.1' minorVersion: $[counter('minorVersion', 1)] prowlarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' sentryOrg: 'servarr' sentryUrl: 'https://sentry.servarr.com' - dotnetVersion: '6.0.427' - nodeVersion: '20.X' + dotnetVersion: '6.0.417' + nodeVersion: '16.X' innoVersion: '6.2.2' windowsImage: 'windows-2022' - linuxImage: 'ubuntu-22.04' - macImage: 'macOS-13' + linuxImage: 'ubuntu-20.04' + macImage: 'macOS-11' trigger: branches: @@ -166,10 +166,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: UseNode@1 + - task: NodeTool@0 displayName: Set Node.js version inputs: - version: $(nodeVersion) + versionSpec: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -1075,10 +1075,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: UseNode@1 + - task: NodeTool@0 displayName: Set Node.js version inputs: - version: $(nodeVersion) + versionSpec: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -1169,12 +1169,12 @@ stages: submodules: true - powershell: Set-Service SCardSvr -StartupType Manual displayName: Enable Windows Test Service - - task: SonarCloudPrepare@3 + - task: SonarCloudPrepare@1 condition: eq(variables['System.PullRequest.IsFork'], 'False') inputs: SonarCloud: 'SonarCloud' organization: 'prowlarr' - scannerMode: 'dotnet' + scannerMode: 'MSBuild' projectKey: 'Prowlarr_Prowlarr' projectName: 'Prowlarr' projectVersion: '$(prowlarrVersion)' @@ -1187,16 +1187,21 @@ stages: ./build.sh --backend -f net6.0 -r win-x64 TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage displayName: Coverage Unit Tests - - task: SonarCloudAnalyze@3 + - task: SonarCloudAnalyze@1 condition: eq(variables['System.PullRequest.IsFork'], 'False') displayName: Publish SonarCloud Results - - task: reportgenerator@5.3.11 + - task: reportgenerator@4 displayName: Generate Coverage Report inputs: reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined' reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges' - publishCodeCoverageResults: true + - task: PublishCodeCoverageResults@1 + displayName: Publish Coverage Report + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: './CoverageResults/combined/Cobertura.xml' + reportDirectory: './CoverageResults/combined/' - stage: Report_Out dependsOn: diff --git a/docs.sh b/docs.sh old mode 100755 new mode 100644 index 38b0e0fbc..ae11bc83f --- a/docs.sh +++ b/docs.sh @@ -1,18 +1,13 @@ -#!/bin/bash -set -e - -FRAMEWORK="net6.0" PLATFORM=$1 -ARCHITECTURE="${2:-x64}" if [ "$PLATFORM" = "Windows" ]; then - RUNTIME="win-$ARCHITECTURE" + RUNTIME="win-x64" elif [ "$PLATFORM" = "Linux" ]; then - RUNTIME="linux-$ARCHITECTURE" + RUNTIME="linux-x64" elif [ "$PLATFORM" = "Mac" ]; then - RUNTIME="osx-$ARCHITECTURE" + RUNTIME="osx-x64" else - echo "Platform must be provided as first argument: Windows, Linux or Mac" + echo "Platform must be provided as first arguement: Windows, Linux or Mac" exit 1 fi @@ -26,23 +21,17 @@ slnFile=src/Prowlarr.sln platform=Posix - if [ "$PLATFORM" = "Windows" ]; then - application=Prowlarr.Console.dll -else - application=Prowlarr.dll -fi - dotnet clean $slnFile -c Debug dotnet clean $slnFile -c Release dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids dotnet new tool-manifest -dotnet tool install --version 7.3.2 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 & +dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 & -sleep 45 +sleep 30 kill %1 diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 56eaaeaab..8d844cb8b 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -357,16 +357,11 @@ module.exports = { ], rules: Object.assign(typescriptEslintRecommended.rules, { - '@typescript-eslint/no-unused-vars': [ - 'error', - { - args: 'after-used', - argsIgnorePattern: '^_', - ignoreRestSiblings: true - } - ], - '@typescript-eslint/explicit-function-return-type': 'off', 'no-shadow': 'off', + // These should be enabled after cleaning things up + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + 'react/prop-types': 'off', 'prettier/prettier': 'error', 'simple-import-sort/imports': [ 'error', @@ -379,41 +374,7 @@ module.exports = { ['^@?\\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' + ] }) }, { diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index ceacc4f04..5336d6583 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -25,7 +25,6 @@ module.exports = (env) => { const config = { mode: isProduction ? 'production' : 'development', devtool: isProduction ? 'source-map' : 'eval-source-map', - target: 'web', stats: { children: false @@ -66,7 +65,7 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: isProduction ? '[name]-[contenthash].js' : '[name].js', + filename: '[name]-[contenthash].js', sourceMapFilename: '[file].map' }, @@ -91,7 +90,7 @@ module.exports = (env) => { new MiniCssExtractPlugin({ filename: 'Content/styles.css', - chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css' + chunkFilename: 'Content/[id]-[chunkhash].css' }), new HtmlWebpackPlugin({ @@ -170,7 +169,7 @@ module.exports = (env) => { loose: true, debug: false, useBuiltIns: 'entry', - corejs: '3.39' + corejs: 3 } ] ] @@ -191,7 +190,7 @@ module.exports = (env) => { options: { importLoaders: 1, modules: { - localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]' + localIdentName: '[name]/[local]/[hash:base64:5]' } } }, diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 89db00f8c..f657adf28 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -16,7 +16,6 @@ const mixinsFiles = [ module.exports = { plugins: [ - 'autoprefixer', ['postcss-mixins', { mixinsFiles }], diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.js similarity index 57% rename from frontend/src/App/App.tsx rename to frontend/src/App/App.js index dba90a697..1eea6e082 100644 --- a/frontend/src/App/App.tsx +++ b/frontend/src/App/App.js @@ -1,30 +1,31 @@ -import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; +import { ConnectedRouter } from 'connected-react-router'; +import PropTypes from 'prop-types'; import React from 'react'; import DocumentTitle from 'react-document-title'; import { Provider } from 'react-redux'; -import { Store } from 'redux'; import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -interface AppProps { - store: Store; - history: ConnectedRouterProps['history']; -} - -function App({ store, history }: AppProps) { +function App({ store, history }) { return ( - - - - + + + + + ); } +App.propTypes = { + store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; + export default App; diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js new file mode 100644 index 000000000..f7a578da2 --- /dev/null +++ b/frontend/src/App/AppRoutes.js @@ -0,0 +1,184 @@ +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 ( + + {/* + Indexers + */} + + + + { + window.Prowlarr.urlBase && + { + return ( + + ); + }} + /> + } + + + + {/* + Search + */} + + + + {/* + Activity + */} + + + + {/* + Settings + */} + + + + + + + + + + + + + + + + + + + + {/* + System + */} + + + + + + + + + + + + + + {/* + Not Found + */} + + + + ); +} + +AppRoutes.propTypes = { + app: PropTypes.func.isRequired +}; + +export default AppRoutes; diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx deleted file mode 100644 index d451a12fb..000000000 --- a/frontend/src/App/AppRoutes.tsx +++ /dev/null @@ -1,117 +0,0 @@ -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 Updates from 'System/Updates/Updates'; -import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; - -function RedirectWithUrlBase() { - return ; -} - -function AppRoutes() { - return ( - - {/* - Indexers - */} - - - - {window.Prowlarr.urlBase && ( - - )} - - - - {/* - Search - */} - - - - {/* - Activity - */} - - - - {/* - Settings - */} - - - - - - - - - - - - - - - - - - - - {/* - System - */} - - - - - - - - - - - - - - {/* - Not Found - */} - - - - ); -} - -export default AppRoutes; diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js new file mode 100644 index 000000000..abc7f8832 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.js @@ -0,0 +1,30 @@ +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 ( + + + + ); +} + +AppUpdatedModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModal.tsx b/frontend/src/App/AppUpdatedModal.tsx deleted file mode 100644 index 696d36fb2..000000000 --- a/frontend/src/App/AppUpdatedModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -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 ( - - - - ); -} - -export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js new file mode 100644 index 000000000..a21afbc5a --- /dev/null +++ b/frontend/src/App/AppUpdatedModalConnector.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import AppUpdatedModal from './AppUpdatedModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(null, createMapDispatchToProps)(AppUpdatedModal); diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js new file mode 100644 index 000000000..8cce1bc16 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -0,0 +1,139 @@ +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 ( + + + {translate('AppUpdated')} + + + +
+ +
+ + { + isPopulated && !error && !!update && +
+ { + !update.changes && +
{translate('MaintenanceRelease')}
+ } + + { + !!update.changes && +
+
+ {translate('WhatsNew')} +
+ + + + +
+ } +
+ } + + { + !isPopulated && !error && + + } +
+ + + + + + +
+ ); +} + +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; diff --git a/frontend/src/App/AppUpdatedModalContent.tsx b/frontend/src/App/AppUpdatedModalContent.tsx deleted file mode 100644 index 0bd5df6d3..000000000 --- a/frontend/src/App/AppUpdatedModalContent.tsx +++ /dev/null @@ -1,145 +0,0 @@ -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 ( - - {translate('AppUpdated')} - - -
- -
- - {isPopulated && !error && !!update ? ( -
- {update.changes ? ( -
- {translate('MaintenanceRelease')} -
- ) : null} - - {update.changes ? ( -
-
{translate('WhatsNew')}
- - - - -
- ) : null} -
- ) : null} - - {!isPopulated && !error ? : null} -
- - - - - - -
- ); -} - -export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js new file mode 100644 index 000000000..97dd0aeb9 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContentConnector.js @@ -0,0 +1,78 @@ +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 ( + + ); + } +} + +AppUpdatedModalContentConnector.propTypes = { + version: PropTypes.string.isRequired, + dispatchFetchUpdates: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector); diff --git a/frontend/src/App/ApplyTheme.js b/frontend/src/App/ApplyTheme.js new file mode 100644 index 000000000..bd4d6a6c8 --- /dev/null +++ b/frontend/src/App/ApplyTheme.js @@ -0,0 +1,50 @@ +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 {children}; +} + +ApplyTheme.propTypes = { + theme: PropTypes.string.isRequired, + children: PropTypes.object.isRequired +}; + +export default connect(createMapStateToProps)(ApplyTheme); diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx deleted file mode 100644 index ec9cd037f..000000000 --- a/frontend/src/App/ApplyTheme.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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; diff --git a/frontend/src/App/ColorImpairedContext.ts b/frontend/src/App/ColorImpairedContext.js similarity index 100% rename from frontend/src/App/ColorImpairedContext.ts rename to frontend/src/App/ColorImpairedContext.js diff --git a/frontend/src/App/ConnectionLostModal.tsx b/frontend/src/App/ConnectionLostModal.js similarity index 54% rename from frontend/src/App/ConnectionLostModal.tsx rename to frontend/src/App/ConnectionLostModal.js index f08f2c0e2..5c08f491f 100644 --- a/frontend/src/App/ConnectionLostModal.tsx +++ b/frontend/src/App/ConnectionLostModal.js @@ -1,4 +1,5 @@ -import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import Button from 'Components/Link/Button'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; @@ -9,31 +10,36 @@ import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './ConnectionLostModal.css'; -interface ConnectionLostModalProps { - isOpen: boolean; -} - -function ConnectionLostModal(props: ConnectionLostModalProps) { - const { isOpen } = props; - - const handleModalClose = useCallback(() => { - location.reload(); - }, []); +function ConnectionLostModal(props) { + const { + isOpen, + onModalClose + } = props; return ( - - - {translate('ConnectionLost')} + + + + {translate('ConnectionLost')} + -
{translate('ConnectionLostToBackend')}
+
+ {translate('ConnectionLostToBackend')} +
{translate('ConnectionLostReconnect')}
- @@ -42,4 +48,9 @@ function ConnectionLostModal(props: ConnectionLostModalProps) { ); } +ConnectionLostModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + export default ConnectionLostModal; diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js new file mode 100644 index 000000000..8ab8e3cd0 --- /dev/null +++ b/frontend/src/App/ConnectionLostModalConnector.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import ConnectionLostModal from './ConnectionLostModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal); diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index f89eb25f7..cabc39b1c 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,6 +1,5 @@ -import Column from 'Components/Table/Column'; import SortDirection from 'Helpers/Props/SortDirection'; -import { FilterBuilderProp, PropertyFilter } from './AppState'; +import { FilterBuilderProp } from './AppState'; export interface Error { responseJSON: { @@ -19,18 +18,10 @@ export interface AppSectionSaveState { } export interface PagedAppSectionState { - page: number; pageSize: number; - totalPages: number; - totalRecords?: number; -} -export interface TableAppSectionState { - columns: Column[]; } export interface AppSectionFilterState { - selectedFilterKey: string; - filters: PropertyFilter[]; filterBuilderProps: FilterBuilderProp[]; } @@ -47,7 +38,6 @@ export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; error: Error; - pendingChanges: Partial; item: T; } diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 0f0e82c0d..2490f739f 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -42,20 +42,7 @@ export interface CustomFilter { filers: PropertyFilter[]; } -export interface AppSectionState { - isConnected: boolean; - isReconnecting: boolean; - version: string; - prevVersion?: string; - dimensions: { - isSmallScreen: boolean; - width: number; - height: number; - }; -} - interface AppState { - app: AppSectionState; commands: CommandAppState; history: HistoryAppState; indexerHistory: IndexerHistoryAppState; diff --git a/frontend/src/App/State/IndexerAppState.ts b/frontend/src/App/State/IndexerAppState.ts index 4c0145d0d..d070986af 100644 --- a/frontend/src/App/State/IndexerAppState.ts +++ b/frontend/src/App/State/IndexerAppState.ts @@ -31,8 +31,6 @@ interface IndexerAppState AppSectionDeleteState, AppSectionSaveState { itemMap: Record; - - isTestingAll: boolean; } export type IndexerStatusAppState = AppSectionState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 33c6c936d..d4152431c 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -7,8 +7,7 @@ import { IndexerCategory } from 'Indexer/Indexer'; import Application from 'typings/Application'; import DownloadClient from 'typings/DownloadClient'; import Notification from 'typings/Notification'; -import General from 'typings/Settings/General'; -import UiSettings from 'typings/Settings/UiSettings'; +import { UiSettings } from 'typings/UiSettings'; export interface AppProfileAppState extends AppSectionState, @@ -25,12 +24,6 @@ export interface ApplicationAppState export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState { - isTestingAll: boolean; -} - -export interface GeneralAppState - extends AppSectionItemState, AppSectionSaveState {} export interface IndexerCategoryAppState @@ -48,7 +41,6 @@ interface SettingsAppState { appProfiles: AppProfileAppState; applications: ApplicationAppState; downloadClients: DownloadClientAppState; - general: GeneralAppState; indexerCategories: IndexerCategoryAppState; notifications: NotificationAppState; ui: UiSettingsAppState; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index 8bc1b03e2..d43c1d0ee 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,19 +1,10 @@ -import Health from 'typings/Health'; import SystemStatus from 'typings/SystemStatus'; -import Task from 'typings/Task'; -import Update from 'typings/Update'; -import AppSectionState, { AppSectionItemState } from './AppSectionState'; +import { AppSectionItemState } from './AppSectionState'; -export type HealthAppState = AppSectionState; export type SystemStatusAppState = AppSectionItemState; -export type TaskAppState = AppSectionState; -export type UpdateAppState = AppSectionState; interface SystemAppState { - health: HealthAppState; status: SystemStatusAppState; - tasks: TaskAppState; - updates: UpdateAppState; } export default SystemAppState; diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index b9b31bf63..45a5beed7 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -12,6 +12,7 @@ export interface CommandBody { lastStartTime: string; trigger: string; suppressMessages: boolean; + seriesId?: number; } interface Command extends ModelBase { diff --git a/frontend/src/Components/Chart/StackedBarChart.js b/frontend/src/Components/Chart/StackedBarChart.js index b69fd8e03..3cca1ba81 100644 --- a/frontend/src/Components/Chart/StackedBarChart.js +++ b/frontend/src/Components/Chart/StackedBarChart.js @@ -46,10 +46,6 @@ class StackedBarChart extends Component { size: 14, family: defaultFontFamily } - }, - tooltip: { - mode: 'index', - position: 'average' } } }, diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js index 931557045..aac01a6b5 100644 --- a/frontend/src/Components/DescriptionList/DescriptionListItem.js +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.js @@ -10,7 +10,6 @@ class DescriptionListItem extends Component { render() { const { - className, titleClassName, descriptionClassName, title, @@ -18,7 +17,7 @@ class DescriptionListItem extends Component { } = this.props; return ( -
+
@@ -36,7 +35,6 @@ class DescriptionListItem extends Component { } DescriptionListItem.propTypes = { - className: PropTypes.string, titleClassName: PropTypes.string, descriptionClassName: PropTypes.string, title: PropTypes.string, diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 51d286311..022cf5a45 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -63,7 +63,11 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
{info.componentStack}
)} -
Version: {window.Prowlarr.version}
+ { +
+ Version: {window.Prowlarr.version} +
+ }
); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js index 0c4a31657..033b9a69a 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js @@ -1,4 +1,3 @@ -import { maxBy } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -51,7 +50,7 @@ class FilterBuilderModalContent extends Component { if (id) { dispatchSetFilter({ selectedFilterKey: id }); } else { - const last = maxBy(customFilters, 'id'); + const last = customFilters[customFilters.length -1]; dispatchSetFilter({ selectedFilterKey: last.id }); } @@ -109,7 +108,7 @@ class FilterBuilderModalContent extends Component { this.setState({ labelErrors: [ { - message: translate('LabelIsRequired') + message: 'Label is required' } ] }); @@ -147,13 +146,13 @@ class FilterBuilderModalContent extends Component { return ( - {translate('CustomFilter')} + Custom Filter
- {translate('Label')} + Label
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index b02844c61..51622509b 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import SelectInput from 'Components/Form/SelectInput'; import IconButton from 'Components/Link/IconButton'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue'; @@ -213,7 +212,7 @@ class FilterBuilderRow extends Component { key: name, value: typeof label === 'function' ? label() : label }; - }).sort(sortByProp('value')); + }).sort((a, b) => a.value.localeCompare(b.value)); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js index d1419327a..a7aed80b6 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { filterBuilderTypes } from 'Helpers/Props'; import * as filterTypes from 'Helpers/Props/filterTypes'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { @@ -38,7 +38,7 @@ function createTagListSelector() { } return acc; - }, []).sort(sortByProp('name')); + }, []).sort(sortByName); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js index 9f378d5a2..7407f729a 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js @@ -37,8 +37,8 @@ class CustomFilter extends Component { dispatchSetFilter } = this.props; - // Assume that delete and then unmounting means the deletion was successful. - // Moving this check to an ancestor would be more accurate, but would have + // Assume that delete and then unmounting means the delete was successful. + // Moving this check to a ancestor would be more accurate, but would have // more boilerplate. if (this.state.isDeleting && id === selectedFilterKey) { dispatchSetFilter({ selectedFilterKey: 'all' }); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index 99cb6ec5c..28eb91599 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -5,7 +5,6 @@ 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 sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import CustomFilter from './CustomFilter'; import styles from './CustomFiltersModalContent.css'; @@ -32,7 +31,7 @@ function CustomFiltersModalContent(props) { { customFilters - .sort((a, b) => sortByProp(a, b, 'label')) + .sort((a, b) => a.label.localeCompare(b.label)) .map((customFilter) => { return ( includeNoChange, (state, { includeMixed }) => includeMixed, (appProfiles, includeNoChange, includeMixed) => { @@ -24,20 +24,16 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - get value() { - return translate('NoChange'); - }, - isDisabled: true + value: translate('NoChange'), + disabled: true }); } if (includeMixed) { values.unshift({ key: 'mixed', - get value() { - return `(${translate('Mixed')})`; - }, - isDisabled: true + value: '(Mixed)', + disabled: true }); } diff --git a/frontend/src/Components/Form/AvailabilitySelectInput.js b/frontend/src/Components/Form/AvailabilitySelectInput.js new file mode 100644 index 000000000..af9bdb2d6 --- /dev/null +++ b/frontend/src/Components/Form/AvailabilitySelectInput.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import SelectInput from './SelectInput'; + +const availabilityOptions = [ + { key: 'announced', value: 'Announced' }, + { key: 'inCinemas', value: 'In Cinemas' }, + { key: 'released', value: 'Released' }, + { key: 'preDB', value: 'PreDB' } +]; + +function AvailabilitySelectInput(props) { + const values = [...availabilityOptions]; + + const { + includeNoChange, + includeMixed + } = props; + + if (includeNoChange) { + values.unshift({ + key: 'noChange', + value: 'No Change', + disabled: true + }); + } + + if (includeMixed) { + values.unshift({ + key: 'mixed', + value: '(Mixed)', + disabled: true + }); + } + + return ( + + ); +} + +AvailabilitySelectInput.propTypes = { + includeNoChange: PropTypes.bool.isRequired, + includeMixed: PropTypes.bool.isRequired +}; + +AvailabilitySelectInput.defaultProps = { + includeNoChange: false, + includeMixed: false +}; + +export default AvailabilitySelectInput; diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index 9cf7a429a..162c79885 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -3,8 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; +import sortByName from 'Utilities/Array/sortByName'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -22,17 +21,16 @@ function createMapStateToProps() { const values = items .filter((downloadClient) => downloadClient.protocol === protocolFilter) - .sort(sortByProp('name')) + .sort(sortByName) .map((downloadClient) => ({ key: downloadClient.id, - value: downloadClient.name, - hint: `(${downloadClient.id})` + value: downloadClient.name })); if (includeAny) { values.unshift({ key: 0, - value: `(${translate('Any')})` + value: '(Any)' }); } diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index 79b1c999c..cc4215025 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import TextInput from './TextInput'; import styles from './EnhancedSelectInput.css'; -const MINIMUM_DISTANCE_FROM_EDGE = 10; - function isArrowKey(keyCode) { return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; } @@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component { // Listeners onComputeMaxHeight = (data) => { + const { + top, + bottom + } = data.offsets.reference; + const windowHeight = window.innerHeight; - data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; + if ((/^botton/).test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } return data; }; @@ -264,29 +271,26 @@ class EnhancedSelectInput extends Component { this.setState({ isOpen: !this.state.isOpen }); }; - onSelect = (newValue) => { - const { name, value, values, onChange } = this.props; - - if (Array.isArray(value)) { - let arrayValue = null; - const index = value.indexOf(newValue); - + onSelect = (value) => { + if (Array.isArray(this.props.value)) { + let newValue = null; + const index = this.props.value.indexOf(value); if (index === -1) { - arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v)); + newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v)); } else { - arrayValue = [...value]; - arrayValue.splice(index, 1); + newValue = [...this.props.value]; + newValue.splice(index, 1); } - onChange({ - name, - value: arrayValue + this.props.onChange({ + name: this.props.name, + value: newValue }); } else { this.setState({ isOpen: false }); - onChange({ - name, - value: newValue + this.props.onChange({ + name: this.props.name, + value }); } }; @@ -453,10 +457,6 @@ class EnhancedSelectInput extends Component { order: 851, enabled: true, fn: this.onComputeMaxHeight - }, - preventOverflow: { - enabled: true, - boundariesElement: 'viewport' } }} > @@ -485,7 +485,7 @@ class EnhancedSelectInput extends Component { values.map((v, index) => { const hasParent = v.parentKey !== undefined; const depth = hasParent ? 1 : 0; - const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey); + const parentSelected = hasParent && value.includes(v.parentKey); return ( {error.errorMessage} - - { - error.detailedDescription ? - } - tooltip={error.detailedDescription} - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> : - null - } ); }) @@ -53,18 +39,6 @@ function Form(props) { kind={kinds.WARNING} > {warning.errorMessage} - - { - warning.detailedDescription ? - } - tooltip={warning.detailedDescription} - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> : - null - } ); }) diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js new file mode 100644 index 000000000..a7145363a --- /dev/null +++ b/frontend/src/Components/Form/FormInputButton.js @@ -0,0 +1,54 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import { kinds } from 'Helpers/Props'; +import styles from './FormInputButton.css'; + +function FormInputButton(props) { + const { + className, + canSpin, + isLastButton, + ...otherProps + } = props; + + if (canSpin) { + return ( + + ); + } + + return ( +
- - diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 6eef54eab..069a4cdf7 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller'; import { icons } from 'Helpers/Props'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import dimensions from 'Styles/Variables/dimensions'; -import HealthStatus from 'System/Status/Health/HealthStatus'; +import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; @@ -87,7 +87,7 @@ const links = [ { title: () => translate('Status'), to: '/system/status', - statusComponent: HealthStatus + statusComponent: HealthStatusConnector }, { title: () => translate('Tasks'), diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css index 409062f97..5e3e3b52c 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css @@ -24,7 +24,6 @@ composes: link; padding: 10px 24px; - padding-left: 35px; } .isActiveLink { @@ -42,6 +41,10 @@ text-align: center; } +.noIcon { + margin-left: 25px; +} + .status { float: right; } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts index 5bf0eb815..77e23c767 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts @@ -8,6 +8,7 @@ interface CssExports { 'isActiveParentLink': string; 'item': string; 'link': string; + 'noIcon': string; 'status': string; } export const cssExports: CssExports; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js index 8d0e4e790..754071c79 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js @@ -63,7 +63,9 @@ class PageSidebarItem extends Component { } - {typeof title === 'function' ? title() : title} + + {typeof title === 'function' ? title() : title} + { !!StatusComponent && diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css index e9a1b666d..0b6918296 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css @@ -22,14 +22,11 @@ display: flex; align-items: center; justify-content: center; - overflow: hidden; height: 24px; } .label { padding: 0 3px; - max-width: 100%; - max-height: 100%; color: var(--toolbarLabelColor); font-size: $extraSmallFontSize; line-height: calc($extraSmallFontSize + 1px); diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js index 675bdfd02..c93603aa9 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js @@ -23,7 +23,6 @@ function PageToolbarButton(props) { isDisabled && styles.isDisabled )} isDisabled={isDisabled || isSpinning} - title={label} {...otherProps} > { - const section = 'settings.applications'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - handleCommand = (body) => { if (body.action === 'sync') { this.props.dispatchFetchCommands(); @@ -160,8 +150,8 @@ class SignalRConnector extends Component { const resource = body.resource; const status = resource.status; - // Both successful and failed commands need to be - // completed, otherwise they spin until they time out. + // Both sucessful and failed commands need to be + // completed, otherwise they spin until they timeout. if (status === 'completed' || status === 'failed') { this.props.dispatchFinishCommand(resource); @@ -170,16 +160,6 @@ class SignalRConnector extends Component { } }; - handleDownloadclient = ({ action, resource }) => { - const section = 'settings.downloadClients'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - handleHealth = () => { this.props.dispatchFetchHealth(); }; @@ -188,33 +168,14 @@ class SignalRConnector extends Component { this.props.dispatchFetchIndexerStatus(); }; - handleIndexer = ({ action, resource }) => { + handleIndexer = (body) => { + const action = body.action; const section = 'indexers'; - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); + if (action === 'updated') { + this.props.dispatchUpdateItem({ section, ...body.resource }); } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleIndexerproxy = ({ action, resource }) => { - const section = 'settings.indexerProxies'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleNotification = ({ action, resource }) => { - const section = 'settings.notifications'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); + this.props.dispatchRemoveItem({ section, id: body.resource.id }); } }; diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index 24674c3fc..31a696df7 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -2,11 +2,9 @@ import React from 'react'; type PropertyFunction = () => T; -// TODO: Convert to generic so `name` can be a type interface Column { name: string; label: string | PropertyFunction | React.ReactNode; - className?: string; columnLabel?: string; isSortable?: boolean; isVisible: boolean; diff --git a/frontend/src/Components/Table/usePaging.ts b/frontend/src/Components/Table/usePaging.ts deleted file mode 100644 index dfebb2355..000000000 --- a/frontend/src/Components/Table/usePaging.ts +++ /dev/null @@ -1,54 +0,0 @@ -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; diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js index fe700b8fe..f4d4e2af4 100644 --- a/frontend/src/Components/TagList.js +++ b/frontend/src/Components/TagList.js @@ -1,15 +1,14 @@ import PropTypes from 'prop-types'; import React from 'react'; import { kinds } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; import Label from './Label'; import styles from './TagList.css'; function TagList({ tags, tagList }) { const sortedTags = tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) - .filter((tag) => !!tag) - .sort(sortByProp('label')); + .filter((t) => t !== undefined) + .sort((a, b) => a.label.localeCompare(b.label)); return (
diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css index e0f1bf5dc..bf31501dd 100644 --- a/frontend/src/Content/Fonts/fonts.css +++ b/frontend/src/Content/Fonts/fonts.css @@ -25,3 +25,14 @@ 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'); } + +/* + * 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'); +} diff --git a/frontend/src/Content/Fonts/text-security-disc.ttf b/frontend/src/Content/Fonts/text-security-disc.ttf new file mode 100644 index 000000000..86038dba8 Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.ttf differ diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff new file mode 100644 index 000000000..bc4cc324b Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.woff differ diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json index f53279dd3..abb5b4647 100644 --- a/frontend/src/Content/Images/Icons/manifest.json +++ b/frontend/src/Content/Images/Icons/manifest.json @@ -15,5 +15,5 @@ "start_url": "../../../../", "theme_color": "#3a3f51", "background_color": "#3a3f51", - "display": "standalone" + "display": "minimal-ui" } diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts deleted file mode 100644 index 417db8178..000000000 --- a/frontend/src/DownloadClient/DownloadProtocol.ts +++ /dev/null @@ -1,3 +0,0 @@ -type DownloadProtocol = 'usenet' | 'torrent' | 'unknown'; - -export default DownloadProtocol; diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts deleted file mode 100644 index 3caf66df2..000000000 --- a/frontend/src/Helpers/Hooks/useCurrentPage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useHistory } from 'react-router-dom'; - -function useCurrentPage() { - const history = useHistory(); - - return history.action === 'POP'; -} - -export default useCurrentPage; diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts deleted file mode 100644 index 24cffb2f1..000000000 --- a/frontend/src/Helpers/Hooks/useModalOpenState.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback, useState } from 'react'; - -export default function useModalOpenState( - initialState: boolean -): [boolean, () => void, () => void] { - const [isOpen, setIsOpen] = useState(initialState); - - const setModalOpen = useCallback(() => { - setIsOpen(true); - }, [setIsOpen]); - - const setModalClosed = useCallback(() => { - setIsOpen(false); - }, [setIsOpen]); - - return [isOpen, setModalOpen, setModalClosed]; -} diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts deleted file mode 100644 index 885c73470..000000000 --- a/frontend/src/Helpers/Props/TooltipPosition.ts +++ /dev/null @@ -1,3 +0,0 @@ -type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; - -export default TooltipPosition; diff --git a/frontend/src/Helpers/Props/align.ts b/frontend/src/Helpers/Props/align.js similarity index 100% rename from frontend/src/Helpers/Props/align.ts rename to frontend/src/Helpers/Props/align.js diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 773748996..834452242 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -43,7 +43,6 @@ import { faChevronCircleRight as fasChevronCircleRight, faChevronCircleUp as fasChevronCircleUp, faCircle as fasCircle, - faCircleDown as fasCircleDown, faCloud as fasCloud, faCloudDownloadAlt as fasCloudDownloadAlt, faCog as fasCog, @@ -142,7 +141,6 @@ export const CHECK_INDETERMINATE = fasMinus; export const CHECK_CIRCLE = fasCheckCircle; export const CHECK_SQUARE = fasSquareCheck; export const CIRCLE = fasCircle; -export const CIRCLE_DOWN = fasCircleDown; export const CIRCLE_OUTLINE = farCircle; export const CLEAR = fasTrashAlt; export const CLIPBOARD = fasCopy; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index f9cd58e6d..6c4564341 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -1,5 +1,6 @@ export const AUTO_COMPLETE = 'autoComplete'; export const APP_PROFILE_SELECT = 'appProfileSelect'; +export const AVAILABILITY_SELECT = 'availabilitySelect'; export const CAPTCHA = 'captcha'; export const CARDIGANNCAPTCHA = 'cardigannCaptcha'; export const CHECK = 'check'; @@ -26,6 +27,7 @@ export const TAG_SELECT = 'tagSelect'; export const all = [ AUTO_COMPLETE, APP_PROFILE_SELECT, + AVAILABILITY_SELECT, CAPTCHA, CARDIGANNCAPTCHA, CHECK, diff --git a/frontend/src/Helpers/Props/kinds.ts b/frontend/src/Helpers/Props/kinds.js similarity index 72% rename from frontend/src/Helpers/Props/kinds.ts rename to frontend/src/Helpers/Props/kinds.js index 7ce606716..b0f5ac87f 100644 --- a/frontend/src/Helpers/Props/kinds.ts +++ b/frontend/src/Helpers/Props/kinds.js @@ -7,6 +7,7 @@ export const PRIMARY = 'primary'; export const PURPLE = 'purple'; export const SUCCESS = 'success'; export const WARNING = 'warning'; +export const QUEUE = 'queue'; export const all = [ DANGER, @@ -18,15 +19,5 @@ export const all = [ PURPLE, SUCCESS, WARNING, -] as const; - -export type Kind = - | 'danger' - | 'default' - | 'disabled' - | 'info' - | 'inverse' - | 'primary' - | 'purple' - | 'success' - | 'warning'; + QUEUE +]; diff --git a/frontend/src/Helpers/Props/sizes.ts b/frontend/src/Helpers/Props/sizes.js similarity index 71% rename from frontend/src/Helpers/Props/sizes.ts rename to frontend/src/Helpers/Props/sizes.js index ca7a50fbf..d7f85df5e 100644 --- a/frontend/src/Helpers/Props/sizes.ts +++ b/frontend/src/Helpers/Props/sizes.js @@ -4,6 +4,4 @@ export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const; - -export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge'; +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; diff --git a/frontend/src/History/Details/HistoryDetails.js b/frontend/src/History/Details/HistoryDetails.js index 6d5ab260e..e0ae06eb1 100644 --- a/frontend/src/History/Details/HistoryDetails.js +++ b/frontend/src/History/Details/HistoryDetails.js @@ -3,7 +3,6 @@ import React from 'react'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import Link from 'Components/Link/Link'; -import formatDateTime from 'Utilities/Date/formatDateTime'; import translate from 'Utilities/String/translate'; import styles from './HistoryDetails.css'; @@ -11,10 +10,7 @@ function HistoryDetails(props) { const { indexer, eventType, - date, - data, - shortDateFormat, - timeFormat + data } = props; if (eventType === 'indexerQuery' || eventType === 'indexerRss') { @@ -25,10 +21,7 @@ function HistoryDetails(props) { limit, offset, source, - host, - url, - elapsedTime, - cached + url } = data; return ( @@ -93,15 +86,6 @@ function HistoryDetails(props) { null } - { - data ? - : - null - } - { data ? : null } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null - } ); } @@ -135,19 +101,10 @@ function HistoryDetails(props) { if (eventType === 'releaseGrabbed') { const { source, - host, grabTitle, - url, - publishedDate, - infoUrl, - downloadClient, - downloadClientName, - elapsedTime, - grabMethod + url } = data; - const downloadClientNameInfo = downloadClientName ?? downloadClient; - return ( { @@ -168,15 +125,6 @@ function HistoryDetails(props) { null } - { - data ? - : - null - } - { data ? {infoUrl}} - /> : - null - } - - { - publishedDate ? - : - null - } - - { - downloadClientNameInfo ? - : - null - } - { data ? : null } - - { - elapsedTime ? - : - null - } - - { - grabMethod ? - : - null - } - - { - date ? - : - null - } ); } if (eventType === 'indexerAuth') { - const { elapsedTime } = data; - return ( : null } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null - } ); } @@ -297,15 +171,6 @@ function HistoryDetails(props) { title={translate('Name')} data={data.query} /> - - { - date ? - : - null - } ); } @@ -313,7 +178,6 @@ function HistoryDetails(props) { HistoryDetails.propTypes = { indexer: PropTypes.object.isRequired, eventType: PropTypes.string.isRequired, - date: PropTypes.string.isRequired, data: PropTypes.object.isRequired, shortDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired diff --git a/frontend/src/History/Details/HistoryDetailsModal.js b/frontend/src/History/Details/HistoryDetailsModal.js index 560955de3..fbc3114ad 100644 --- a/frontend/src/History/Details/HistoryDetailsModal.js +++ b/frontend/src/History/Details/HistoryDetailsModal.js @@ -29,7 +29,6 @@ function HistoryDetailsModal(props) { isOpen, eventType, indexer, - date, data, shortDateFormat, timeFormat, @@ -50,7 +49,6 @@ function HistoryDetailsModal(props) { ); } @@ -332,21 +331,6 @@ class HistoryRow extends Component { ); } - if (name === 'host') { - return ( - - { - data.host ? - data.host : - null - } - - ); - } - if (name === 'elapsedTime') { return ( ); } @@ -410,7 +393,6 @@ class HistoryRow extends Component { {value}; } else if (type === 'tmdb') { link = ( - - {value} - + {value} ); } else if (type === 'tvdb') { link = ( diff --git a/frontend/src/Indexer/Add/AddIndexerModal.js b/frontend/src/Indexer/Add/AddIndexerModal.js new file mode 100644 index 000000000..9344c8130 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModal.js @@ -0,0 +1,31 @@ +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 ( + + + + ); +} + +AddIndexerModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + onSelectIndexer: PropTypes.func.isRequired +}; + +export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModal.tsx b/frontend/src/Indexer/Add/AddIndexerModal.tsx deleted file mode 100644 index be22eec57..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModal.tsx +++ /dev/null @@ -1,44 +0,0 @@ -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 ( - - - - ); -} - -export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css b/frontend/src/Indexer/Add/AddIndexerModalContent.css index e824c5475..a58eccfbc 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css @@ -19,16 +19,10 @@ margin-bottom: 16px; } -.notice { - composes: alert from '~Components/Alert.css'; - - margin-bottom: 20px; -} - .alert { composes: alert from '~Components/Alert.css'; - text-align: center; + margin-bottom: 20px; } .scroller { diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts index 5978832e4..cbedc72a4 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts @@ -10,7 +10,6 @@ interface CssExports { 'indexers': string; 'modalBody': string; 'modalFooter': string; - 'notice': string; 'scroller': string; } export const cssExports: CssExports; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.js b/frontend/src/Indexer/Add/AddIndexerModalContent.js new file mode 100644 index 000000000..f4050560e --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.js @@ -0,0 +1,324 @@ +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 ( + + + {translate('AddIndexer')} + + + + + +
+
+ + this.setState({ filterProtocols: value })} + /> +
+ +
+ + this.setState({ filterLanguages: value })} + /> +
+ +
+ + this.setState({ filterPrivacyLevels: value })} + /> +
+ +
+ + this.setState({ filterCategories: value })} + /> +
+
+ + +
+ {translate('ProwlarrSupportsAnyIndexer')} +
+
+ + + { + isFetching ? : null + } + { + error ? {errorMessage} : null + } + { + isPopulated && !!indexers.length ? + + + { + filteredIndexers.map((indexer) => ( + + )) + } + +
: + null + } + { + isPopulated && !!indexers.length && !filteredIndexers.length ? + + {translate('NoIndexersFound')} + : + null + } +
+
+ + +
+ { + isPopulated ? + translate('CountIndexersAvailable', { count: filteredIndexers.length }) : + null + } +
+ +
+ +
+
+
+ ); + } +} + +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; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx deleted file mode 100644 index be1413769..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx +++ /dev/null @@ -1,434 +0,0 @@ -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([]); - const [filterLanguages, setFilterLanguages] = useState([]); - const [filterPrivacyLevels, setFilterPrivacyLevels] = useState([]); - const [filterCategories, setFilterCategories] = useState([]); - - 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( - (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 ( - - {translate('AddIndexer')} - - - - -
-
- - - -
- -
- - - -
- -
- - -
- -
- - - -
-
- - -
{translate('ProwlarrSupportsAnyIndexer')}
-
- - - {isFetching ? : null} - - {error ? ( - - {errorMessage} - - ) : null} - - {isPopulated && !!indexers.length ? ( - - - {filteredIndexers.map((indexer) => ( - - ))} - -
- ) : null} - - {isPopulated && !!indexers.length && !filteredIndexers.length ? ( - - {translate('NoIndexersFound')} - - ) : null} -
-
- - -
- {isPopulated - ? translate('CountIndexersAvailable', { - count: filteredIndexers.length, - }) - : null} -
- -
- -
-
-
- ); -} - -export default AddIndexerModalContent; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js new file mode 100644 index 000000000..a422e0a03 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js @@ -0,0 +1,94 @@ +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 ( + + ); + } +} + +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); diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.tsx b/frontend/src/Indexer/Add/SelectIndexerRow.tsx index 157050e41..ab6850573 100644 --- a/frontend/src/Indexer/Add/SelectIndexerRow.tsx +++ b/frontend/src/Indexer/Add/SelectIndexerRow.tsx @@ -2,19 +2,18 @@ import React, { useCallback } from 'react'; import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowButton from 'Components/Table/TableRowButton'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { icons } from 'Helpers/Props'; import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel'; -import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel'; import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer'; +import { IndexerCapabilities } from 'Indexer/Indexer'; +import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; import styles from './SelectIndexerRow.css'; interface SelectIndexerRowProps { name: string; - protocol: DownloadProtocol; - privacy: IndexerPrivacy; + protocol: string; + privacy: string; language: string; description: string; capabilities: IndexerCapabilities; @@ -64,9 +63,7 @@ function SelectIndexerRow(props: SelectIndexerRowProps) { {description} - - - + {translate(firstCharToUpper(privacy))} diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContent.js b/frontend/src/Indexer/Edit/EditIndexerModalContent.js index 7dabc50d9..b09b9e656 100644 --- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js +++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js @@ -97,7 +97,7 @@ function EditIndexerModalContent(props) { @@ -144,7 +144,6 @@ function EditIndexerModalContent(props) { }) : null } - { ); const [isSelectMode, setIsSelectMode] = useState(false); - useEffect(() => { - dispatch(fetchIndexers()); - dispatch(fetchIndexerStatus()); - }, [dispatch]); - const onAddIndexerPress = useCallback(() => { setIsAddIndexerModalOpen(true); }, [setIsAddIndexerModalOpen]); diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx index 9d42aa389..b0bba5b94 100644 --- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx @@ -19,7 +19,6 @@ interface SavePayload { seedRatio?: number; seedTime?: number; packSeedTime?: number; - preferMagnetUrl?: boolean; } interface EditIndexerModalContentProps { @@ -36,7 +35,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'true', @@ -66,9 +65,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { const [packSeedTime, setPackSeedTime] = useState( null ); - const [preferMagnetUrl, setPreferMagnetUrl] = useState< - null | string | boolean - >(null); const save = useCallback(() => { let hasChanges = false; @@ -109,11 +105,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { payload.packSeedTime = packSeedTime as number; } - if (preferMagnetUrl !== null) { - hasChanges = true; - payload.preferMagnetUrl = preferMagnetUrl === 'true'; - } - if (hasChanges) { onSavePress(payload); } @@ -127,7 +118,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { seedRatio, seedTime, packSeedTime, - preferMagnetUrl, onSavePress, onModalClose, ]); @@ -156,9 +146,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { case 'packSeedTime': setPackSeedTime(value); break; - case 'preferMagnetUrl': - setPreferMagnetUrl(value); - break; default: console.warn(`EditIndexersModalContent Unknown Input: '${name}'`); } @@ -237,7 +224,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { name="seedRatio" value={seedRatio} helpText={translate('SeedRatioHelpText')} - isFloat={true} onChange={onInputChange} /> @@ -267,18 +253,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { onChange={onInputChange} /> - - - {translate('PreferMagnetUrl')} - - - diff --git a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx index 8e30532cc..c832806ed 100644 --- a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx +++ b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx @@ -2,7 +2,6 @@ import { uniqBy } from 'lodash'; import React from 'react'; import Label from 'Components/Label'; import { IndexerCapabilities } from 'Indexer/Indexer'; -import translate from 'Utilities/String/translate'; interface CapabilitiesLabelProps { capabilities: IndexerCapabilities; @@ -39,7 +38,7 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) { ); })} - {filteredList.length === 0 ? : null} + {filteredList.length === 0 ? : null} ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css index a20efded3..a09d0218e 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css @@ -29,8 +29,7 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime, -.preferMagnetUrl { +.packSeedTime { composes: cell; flex: 0 0 90px; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts index 42821bd74..5feb0c35d 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts @@ -11,7 +11,6 @@ interface CssExports { 'id': string; 'minimumSeeders': string; 'packSeedTime': string; - 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx index e4c3cd32e..9e83e9b8d 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { useSelect } from 'App/SelectContext'; -import CheckInput from 'Components/Form/CheckInput'; +import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; @@ -15,10 +15,10 @@ import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItem import Indexer from 'Indexer/Indexer'; import IndexerTitleLink from 'Indexer/IndexerTitleLink'; import { SelectStateInputProps } from 'typings/props'; +import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; import CapabilitiesLabel from './CapabilitiesLabel'; import IndexerStatusCell from './IndexerStatusCell'; -import PrivacyLabel from './PrivacyLabel'; import ProtocolLabel from './ProtocolLabel'; import styles from './IndexerIndexRow.css'; @@ -75,10 +75,6 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime') ?.value ?? undefined; - const preferMagnetUrl = - fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl') - ?.value ?? undefined; - const rssUrl = `${window.location.origin}${ window.Prowlarr.urlBase }/${id}/api?apikey=${encodeURIComponent( @@ -107,10 +103,6 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { setIsDeleteIndexerModalOpen(false); }, [setIsDeleteIndexerModalOpen]); - const checkInputCallback = useCallback(() => { - // Mock handler to satisfy `onChange` being required for `CheckInput`. - }, []); - const onSelectedChange = useCallback( ({ id, value, shiftKey }: SelectStateInputProps) => { selectDispatch({ @@ -183,7 +175,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { if (name === 'privacy') { return ( - + ); } @@ -286,21 +278,6 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { ); } - if (name === 'preferMagnetUrl') { - return ( - - {preferMagnetUrl === undefined ? null : ( - - )} - - ); - } - if (name === 'actions') { return ( columns ); -function Row({ index, style, data }: ListChildComponentProps) { +const Row: React.FC> = ({ + index, + style, + data, +}) => { const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data; if (index >= items.length) { @@ -73,7 +77,7 @@ function Row({ index, style, data }: ListChildComponentProps) { />
); -} +}; function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css index 839cd49ff..185ba0ef7 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css @@ -22,8 +22,7 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime, -.preferMagnetUrl { +.packSeedTime { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; flex: 0 0 90px; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts index 020d61f27..f6a54fa25 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts @@ -8,7 +8,6 @@ interface CssExports { 'id': string; 'minimumSeeders': string; 'packSeedTime': string; - 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx index 3aa087790..6155929df 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { Fragment, useCallback } from 'react'; import { useSelector } from 'react-redux'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -32,17 +32,19 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) { ); return ( - - {translate('ShowSearch')} + + + {translate('ShowSearch')} - - + + + ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx index 1a2350302..a3d694e9d 100644 --- a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx @@ -8,30 +8,6 @@ import translate from 'Utilities/String/translate'; import DisabledIndexerInfo from './DisabledIndexerInfo'; 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 { className: string; enabled: boolean; @@ -54,14 +30,22 @@ function IndexerStatusCell(props: IndexerStatusCellProps) { ...otherProps } = props; + const enableKind = redirect ? kinds.INFO : kinds.SUCCESS; + const enableIcon = redirect ? icons.REDIRECT : icons.CHECK; + const enableTitle = redirect + ? translate('EnabledRedirected') + : translate('Enabled'); + return ( - + { + + } {status ? ( - {translate(firstCharToUpper(privacy))} - - ); -} - -export default PrivacyLabel; diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.css b/frontend/src/Indexer/Index/Table/ProtocolLabel.css index c94e383b1..110c7e01c 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css @@ -11,7 +11,3 @@ border-color: var(--usenetColor); background-color: var(--usenetColor); } - -.unknown { - composes: label from '~Components/Label.css'; -} diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts b/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts index ba0cb260d..f3b389e3d 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts @@ -2,7 +2,6 @@ // Please do not change this file! interface CssExports { 'torrent': string; - 'unknown': string; 'usenet': string; } export const cssExports: CssExports; diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx index c1824452a..08009109e 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx @@ -1,15 +1,18 @@ import React from 'react'; import Label from 'Components/Label'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import styles from './ProtocolLabel.css'; interface ProtocolLabelProps { - protocol: DownloadProtocol; + protocol: string; } -function ProtocolLabel({ protocol }: ProtocolLabelProps) { +function ProtocolLabel(props: ProtocolLabelProps) { + const { protocol } = props; + const protocolName = protocol === 'usenet' ? 'nzb' : protocol; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ts(7053) return ; } diff --git a/frontend/src/Indexer/Indexer.ts b/frontend/src/Indexer/Indexer.ts index c363d472c..96a67f446 100644 --- a/frontend/src/Indexer/Indexer.ts +++ b/frontend/src/Indexer/Indexer.ts @@ -1,5 +1,4 @@ import ModelBase from 'App/ModelBase'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; export interface IndexerStatus extends ModelBase { disabledTill: Date; @@ -25,8 +24,6 @@ export interface IndexerCapabilities extends ModelBase { categories: IndexerCategory[]; } -export type IndexerPrivacy = 'public' | 'semiPrivate' | 'private'; - export interface IndexerField extends ModelBase { order: number; name: string; @@ -39,7 +36,6 @@ export interface IndexerField extends ModelBase { interface Indexer extends ModelBase { name: string; - definitionName: string; description: string; encoding: string; language: string; @@ -50,8 +46,8 @@ interface Indexer extends ModelBase { supportsSearch: boolean; supportsRedirect: boolean; supportsPagination: boolean; - protocol: DownloadProtocol; - privacy: IndexerPrivacy; + protocol: string; + privacy: string; priority: number; fields: IndexerField[]; tags: number[]; diff --git a/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx b/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx index 28d45654c..455b5106d 100644 --- a/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx +++ b/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx @@ -68,14 +68,13 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) { key={parameter.key} title={parameter.title} value={data[parameter.key as keyof HistoryData].toString()} - queryType={data.queryType} /> ); })}
- + {data.source ? data.source : null} @@ -84,15 +83,14 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) { { + return { + indexer, + }; + } + ); +} + +const tabs = ['details', 'categories', 'history', 'stats']; interface IndexerInfoModalContentProps { indexerId: number; @@ -38,7 +50,9 @@ interface IndexerInfoModalContentProps { } function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { - const { indexerId, onModalClose, onCloneIndexerPress } = props; + const { indexerId, onCloneIndexerPress } = props; + + const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId)); const { id, @@ -50,55 +64,54 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { fields, tags, protocol, - privacy, capabilities = {} as IndexerCapabilities, - } = useIndexer(indexerId) as Indexer; + } = indexer as Indexer; - const [selectedTab, setSelectedTab] = useState(TABS[0]); - const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); - const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = - useState(false); - - const handleTabSelect = useCallback( - (selectedIndex: number) => { - const selectedTab = TABS[selectedIndex]; - setSelectedTab(selectedTab); - }, - [setSelectedTab] - ); - - const handleEditIndexerPress = useCallback(() => { - setIsEditIndexerModalOpen(true); - }, [setIsEditIndexerModalOpen]); - - const handleEditIndexerModalClose = useCallback(() => { - setIsEditIndexerModalOpen(false); - }, [setIsEditIndexerModalOpen]); - - const handleDeleteIndexerPress = useCallback(() => { - setIsEditIndexerModalOpen(false); - setIsDeleteIndexerModalOpen(true); - }, [setIsDeleteIndexerModalOpen]); - - const handleDeleteIndexerModalClose = useCallback(() => { - setIsDeleteIndexerModalOpen(false); - onModalClose(); - }, [setIsDeleteIndexerModalOpen, onModalClose]); - - const handleCloneIndexerPressWrapper = useCallback(() => { - onCloneIndexerPress(id); - onModalClose(); - }, [id, onCloneIndexerPress, onModalClose]); + const { onModalClose } = props; const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined); - const indexerUrl = baseUrl?.replace(/(:\/\/)api\./, '$1'); - const vipExpiration = fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined; + const [selectedTab, setSelectedTab] = useState(tabs[0]); + const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); + const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = + useState(false); + + const onTabSelect = useCallback( + (index: number) => { + const selectedTab = tabs[index]; + setSelectedTab(selectedTab); + }, + [setSelectedTab] + ); + + const onEditIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(true); + }, [setIsEditIndexerModalOpen]); + + const onEditIndexerModalClose = useCallback(() => { + setIsEditIndexerModalOpen(false); + }, [setIsEditIndexerModalOpen]); + + const onDeleteIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(false); + setIsDeleteIndexerModalOpen(true); + }, [setIsDeleteIndexerModalOpen]); + + const onDeleteIndexerModalClose = useCallback(() => { + setIsDeleteIndexerModalOpen(false); + onModalClose(); + }, [setIsDeleteIndexerModalOpen, onModalClose]); + + const onCloneIndexerPressWrapper = useCallback(() => { + onCloneIndexerPress(id); + onModalClose(); + }, [id, onCloneIndexerPress, onModalClose]); + return ( {`${name}`} @@ -106,8 +119,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { @@ -147,11 +160,6 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { title={translate('Language')} data={language ?? '-'} /> - : '-'} - /> {vipExpiration ? ( - {indexerUrl ? ( - {indexerUrl} + {baseUrl ? ( + + {baseUrl.replace(/(:\/\/)api\./, '$1')} + ) : ( '-' )} @@ -348,16 +358,16 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { -
- +
@@ -365,14 +375,14 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
); diff --git a/frontend/src/Indexer/NoIndexer.css b/frontend/src/Indexer/NoIndexer.css index 4ad534de3..38a01f391 100644 --- a/frontend/src/Indexer/NoIndexer.css +++ b/frontend/src/Indexer/NoIndexer.css @@ -1,6 +1,4 @@ .message { - composes: alert from '~Components/Alert.css'; - margin-top: 10px; margin-bottom: 30px; text-align: center; diff --git a/frontend/src/Indexer/NoIndexer.tsx b/frontend/src/Indexer/NoIndexer.tsx index bf5afa1fe..75650cad6 100644 --- a/frontend/src/Indexer/NoIndexer.tsx +++ b/frontend/src/Indexer/NoIndexer.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; @@ -15,9 +14,11 @@ function NoIndexer(props: NoIndexerProps) { if (totalItems > 0) { return ( - - {translate('AllIndexersHiddenDueToFilter')} - +
+
+ {translate('AllIndexersHiddenDueToFilter')} +
+
); } @@ -28,7 +29,7 @@ function NoIndexer(props: NoIndexerProps) {
-
diff --git a/frontend/src/Indexer/Stats/IndexerStats.tsx b/frontend/src/Indexer/Stats/IndexerStats.tsx index bccd49cbe..f7b4a9413 100644 --- a/frontend/src/Indexer/Stats/IndexerStats.tsx +++ b/frontend/src/Indexer/Stats/IndexerStats.tsx @@ -32,30 +32,23 @@ import IndexerStatsFilterModal from './IndexerStatsFilterModal'; import styles from './IndexerStats.css'; function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) { - const statistics = [...indexerStats].sort((a, b) => - a.averageResponseTime === b.averageResponseTime - ? b.averageGrabResponseTime - a.averageGrabResponseTime - : b.averageResponseTime - a.averageResponseTime - ); + const data = indexerStats.map((indexer) => { + return { + label: indexer.indexerName, + value: indexer.averageResponseTime, + }; + }); - return { - labels: statistics.map((indexer) => indexer.indexerName), - datasets: [ - { - label: translate('AverageQueries'), - data: statistics.map((indexer) => indexer.averageResponseTime), - }, - { - label: translate('AverageGrabs'), - data: statistics.map((indexer) => indexer.averageGrabResponseTime), - }, - ], - }; + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; } function getFailureRateData(indexerStats: IndexerStatsIndexer[]) { - const data = [...indexerStats] - .map((indexer) => ({ + const data = indexerStats.map((indexer) => { + return { label: indexer.indexerName, value: (indexer.numberOfFailedQueries + @@ -66,102 +59,109 @@ function getFailureRateData(indexerStats: IndexerStatsIndexer[]) { indexer.numberOfRssQueries + indexer.numberOfAuthQueries + indexer.numberOfGrabs), - })) - .filter((s) => s.value > 0); + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) { - const statistics = [...indexerStats] - .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), + const data = { + labels: indexerStats.map((indexer) => indexer.indexerName), datasets: [ { label: translate('SearchQueries'), - data: statistics.map((indexer) => indexer.numberOfQueries), + data: indexerStats.map((indexer) => indexer.numberOfQueries), }, { label: translate('RssQueries'), - data: statistics.map((indexer) => indexer.numberOfRssQueries), + data: indexerStats.map((indexer) => indexer.numberOfRssQueries), }, { label: translate('AuthQueries'), - data: statistics.map((indexer) => indexer.numberOfAuthQueries), + data: indexerStats.map((indexer) => indexer.numberOfAuthQueries), }, ], }; + + return data; } function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) { - const data = [...indexerStats] - .map((indexer) => ({ + const data = indexerStats.map((indexer) => { + return { label: indexer.indexerName, value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs, - })) - .filter((s) => s.value > 0); + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfGrabs, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfGrabs, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfQueries, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfQueries, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getHostGrabsData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfGrabs, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfGrabs, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getHostQueryData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfQueries, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfQueries, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } @@ -241,9 +241,9 @@ function IndexerStats() { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} + onFilterSelect={onFilterSelect} filterModalConnectorComponent={IndexerStatsFilterModal} isDisabled={false} - onFilterSelect={onFilterSelect} /> @@ -294,7 +294,7 @@ function IndexerStats() {
- state.indexers.itemMap, - (state: AppState) => state.indexers.items, - (itemMap, allIndexers) => { - return indexerId ? allIndexers[itemMap[indexerId]] : undefined; - } - ); -} - -function useIndexer(indexerId?: number) { - return useSelector(createIndexerSelector(indexerId)); -} - -export default useIndexer; diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css b/frontend/src/Search/Mobile/SearchIndexOverview.css index e29ff1ef9..4e184bd0a 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css @@ -47,42 +47,3 @@ $hoverScale: 1.05; right: 0; 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; -} diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts b/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts index 68256eb25..266cf7fca 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts @@ -4,13 +4,9 @@ interface CssExports { 'actions': string; 'container': string; 'content': string; - 'downloadIcon': string; - 'downloadLink': string; 'indexerRow': string; 'info': string; 'infoRow': string; - 'interactiveIcon': string; - 'manualDownloadContent': string; 'title': string; 'titleRow': string; } diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.js b/frontend/src/Search/Mobile/SearchIndexOverview.js new file mode 100644 index 000000000..1a14ae66c --- /dev/null +++ b/frontend/src/Search/Mobile/SearchIndexOverview.js @@ -0,0 +1,234 @@ +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 ( +
+
+
+
+
+ + + +
+ +
+ + + { + downloadUrl || magnetUrl ? + : + null + } +
+
+
+ {indexer} +
+
+ + + { + protocol === 'torrent' && + + } + + + + + + + + { + indexerFlags.length ? + indexerFlags + .sort((a, b) => a.localeCompare(b)) + .map((flag, index) => { + return ( + + ); + }) : + null + } +
+
+
+
+ ); + } +} + +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; diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.tsx b/frontend/src/Search/Mobile/SearchIndexOverview.tsx deleted file mode 100644 index 21a42d70c..000000000 --- a/frontend/src/Search/Mobile/SearchIndexOverview.tsx +++ /dev/null @@ -1,264 +0,0 @@ -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 ( - <> -
-
-
-
-
- - - -
- -
- - - {downloadClients.length > 1 ? ( - -
- - - -
- - ) : null} - - {downloadUrl || magnetUrl ? ( - - ) : null} -
-
-
{indexer}
-
- - - {protocol === 'torrent' && ( - - )} - - - - - - - - {indexerFlags.length - ? indexerFlags - .sort((a, b) => - a.localeCompare(b, undefined, { numeric: true }) - ) - .map((flag, index) => { - return ( - - ); - }) - : null} -
-
-
-
- - - - ); -} - -export default SearchIndexOverview; diff --git a/frontend/src/Search/NoSearchResults.css b/frontend/src/Search/NoSearchResults.css index f17dd633e..eff6272f7 100644 --- a/frontend/src/Search/NoSearchResults.css +++ b/frontend/src/Search/NoSearchResults.css @@ -1,6 +1,4 @@ .message { - composes: alert from '~Components/Alert.css'; - margin-top: 10px; margin-bottom: 30px; text-align: center; diff --git a/frontend/src/Search/NoSearchResults.tsx b/frontend/src/Search/NoSearchResults.tsx index 46fbc85e0..4ffd1d7fd 100644 --- a/frontend/src/Search/NoSearchResults.tsx +++ b/frontend/src/Search/NoSearchResults.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import Alert from 'Components/Alert'; -import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './NoSearchResults.css'; @@ -13,16 +11,18 @@ function NoSearchResults(props: NoSearchResultsProps) { if (totalItems > 0) { return ( - - {translate('AllSearchResultsHiddenByFilter')} - +
+
+ {translate('AllIndexersHiddenDueToFilter')} +
+
); } return ( - - {translate('NoSearchResultsFound')} - +
+
{translate('NoSearchResultsFound')}
+
); } diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx deleted file mode 100644 index 7d623decd..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { sizes } from 'Helpers/Props'; -import SelectDownloadClientModalContent from './SelectDownloadClientModalContent'; - -interface SelectDownloadClientModalProps { - isOpen: boolean; - protocol: DownloadProtocol; - modalTitle: string; - onDownloadClientSelect(downloadClientId: number): void; - onModalClose(): void; -} - -function SelectDownloadClientModal(props: SelectDownloadClientModalProps) { - const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } = - props; - - return ( - - - - ); -} - -export default SelectDownloadClientModal; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx deleted file mode 100644 index 63e15808f..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import Alert from 'Components/Alert'; -import Form from 'Components/Form/Form'; -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 DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { kinds } from 'Helpers/Props'; -import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; -import translate from 'Utilities/String/translate'; -import SelectDownloadClientRow from './SelectDownloadClientRow'; - -interface SelectDownloadClientModalContentProps { - protocol: DownloadProtocol; - modalTitle: string; - onDownloadClientSelect(downloadClientId: number): void; - onModalClose(): void; -} - -function SelectDownloadClientModalContent( - props: SelectDownloadClientModalContentProps -) { - const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props; - - const { isFetching, isPopulated, error, items } = useSelector( - createEnabledDownloadClientsSelector(protocol) - ); - - return ( - - - {translate('SelectDownloadClientModalTitle', { modalTitle })} - - - - {isFetching ? : null} - - {!isFetching && error ? ( - - {translate('DownloadClientsLoadError')} - - ) : null} - - {isPopulated && !error ? ( -
- {items.map((downloadClient) => { - const { id, name, priority } = downloadClient; - - return ( - - ); - })} - - ) : null} -
- - - - -
- ); -} - -export default SelectDownloadClientModalContent; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css deleted file mode 100644 index 6525db977..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css +++ /dev/null @@ -1,6 +0,0 @@ -.downloadClient { - display: flex; - justify-content: space-between; - padding: 8px; - border-bottom: 1px solid var(--borderColor); -} diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx deleted file mode 100644 index 6f98d60b4..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useCallback } from 'react'; -import Link from 'Components/Link/Link'; -import translate from 'Utilities/String/translate'; -import styles from './SelectDownloadClientRow.css'; - -interface SelectSeasonRowProps { - id: number; - name: string; - priority: number; - onDownloadClientSelect(downloadClientId: number): unknown; -} - -function SelectDownloadClientRow(props: SelectSeasonRowProps) { - const { id, name, priority, onDownloadClientSelect } = props; - - const onSeasonSelectWrapper = useCallback(() => { - onDownloadClientSelect(id); - }, [id, onDownloadClientSelect]); - - return ( - -
{name}
-
{translate('PrioritySettings', { priority })}
- - ); -} - -export default SelectDownloadClientRow; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.css b/frontend/src/Search/OverrideMatch/OverrideMatchData.css deleted file mode 100644 index bd4d2f788..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.css +++ /dev/null @@ -1,17 +0,0 @@ -.link { - composes: link from '~Components/Link/Link.css'; - - width: 100%; -} - -.placeholder { - display: inline-block; - margin: -2px 0; - width: 100%; - outline: 2px dashed var(--dangerColor); - outline-offset: -2px; -} - -.optional { - outline: 2px dashed var(--gray); -} diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts b/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts deleted file mode 100644 index dd3ac4575..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'link': string; - 'optional': string; - 'placeholder': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx deleted file mode 100644 index 82d6bd812..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import Link from 'Components/Link/Link'; -import styles from './OverrideMatchData.css'; - -interface OverrideMatchDataProps { - value?: string | number | JSX.Element | JSX.Element[]; - isDisabled?: boolean; - isOptional?: boolean; - onPress: () => void; -} - -function OverrideMatchData(props: OverrideMatchDataProps) { - const { value, isDisabled = false, isOptional, onPress } = props; - - return ( - - {(value == null || (Array.isArray(value) && value.length === 0)) && - !isDisabled ? ( - -   - - ) : ( - value - )} - - ); -} - -export default OverrideMatchData; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx deleted file mode 100644 index 16d62ea7c..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { sizes } from 'Helpers/Props'; -import OverrideMatchModalContent from './OverrideMatchModalContent'; - -interface OverrideMatchModalProps { - isOpen: boolean; - title: string; - indexerId: number; - guid: string; - protocol: DownloadProtocol; - isGrabbing: boolean; - grabError?: string; - onModalClose(): void; -} - -function OverrideMatchModal(props: OverrideMatchModalProps) { - const { - isOpen, - title, - indexerId, - guid, - protocol, - isGrabbing, - grabError, - onModalClose, - } = props; - - return ( - - - - ); -} - -export default OverrideMatchModal; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css deleted file mode 100644 index a5b4b8d52..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css +++ /dev/null @@ -1,49 +0,0 @@ -.label { - composes: label from '~Components/Label.css'; - - cursor: pointer; -} - -.item { - display: block; - margin-bottom: 5px; - margin-left: 50px; -} - -.footer { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - display: flex; - justify-content: space-between; - overflow: hidden; -} - -.error { - margin-right: 20px; - color: var(--dangerColor); - word-break: break-word; -} - -.buttons { - display: flex; -} - -@media only screen and (max-width: $breakpointSmall) { - .item { - margin-left: 0; - } - - .footer { - display: block; - } - - .error { - margin-right: 0; - margin-bottom: 10px; - } - - .buttons { - justify-content: space-between; - flex-grow: 1; - } -} diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts deleted file mode 100644 index 79c77d6b5..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'buttons': string; - 'error': string; - 'footer': string; - 'item': string; - 'label': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx deleted file mode 100644 index fbe0ec450..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -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 DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import usePrevious from 'Helpers/Hooks/usePrevious'; -import { grabRelease } from 'Store/Actions/releaseActions'; -import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; -import translate from 'Utilities/String/translate'; -import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal'; -import OverrideMatchData from './OverrideMatchData'; -import styles from './OverrideMatchModalContent.css'; - -type SelectType = 'select' | 'downloadClient'; - -interface OverrideMatchModalContentProps { - indexerId: number; - title: string; - guid: string; - protocol: DownloadProtocol; - isGrabbing: boolean; - grabError?: string; - onModalClose(): void; -} - -function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { - const modalTitle = translate('ManualGrab'); - const { - indexerId, - title, - guid, - protocol, - isGrabbing, - grabError, - onModalClose, - } = props; - - const [downloadClientId, setDownloadClientId] = useState(null); - const [selectModalOpen, setSelectModalOpen] = useState( - null - ); - const previousIsGrabbing = usePrevious(isGrabbing); - - const dispatch = useDispatch(); - const { items: downloadClients } = useSelector( - createEnabledDownloadClientsSelector(protocol) - ); - - const onSelectModalClose = useCallback(() => { - setSelectModalOpen(null); - }, [setSelectModalOpen]); - - const onSelectDownloadClientPress = useCallback(() => { - setSelectModalOpen('downloadClient'); - }, [setSelectModalOpen]); - - const onDownloadClientSelect = useCallback( - (downloadClientId: number) => { - setDownloadClientId(downloadClientId); - setSelectModalOpen(null); - }, - [setDownloadClientId, setSelectModalOpen] - ); - - const onGrabPress = useCallback(() => { - dispatch( - grabRelease({ - indexerId, - guid, - downloadClientId, - }) - ); - }, [indexerId, guid, downloadClientId, dispatch]); - - useEffect(() => { - if (!isGrabbing && previousIsGrabbing) { - onModalClose(); - } - }, [isGrabbing, previousIsGrabbing, onModalClose]); - - useEffect( - () => { - dispatch(fetchDownloadClients()); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return ( - - - {translate('OverrideGrabModalTitle', { title })} - - - - - {downloadClients.length > 1 ? ( - downloadClient.id === downloadClientId - )?.name ?? translate('Default') - } - onPress={onSelectDownloadClientPress} - /> - } - /> - ) : null} - - - - -
{grabError}
- -
- - - - {translate('GrabRelease')} - -
-
- - -
- ); -} - -export default OverrideMatchModalContent; diff --git a/frontend/src/Search/SearchFooter.js b/frontend/src/Search/SearchFooter.js index 872328446..5e949fc6e 100644 --- a/frontend/src/Search/SearchFooter.js +++ b/frontend/src/Search/SearchFooter.js @@ -212,11 +212,7 @@ class SearchFooter extends Component { name="searchQuery" value={searchQuery} buttons={ - + @@ -279,7 +275,6 @@ class SearchFooter extends Component { } @@ -306,7 +306,7 @@ class SearchIndex extends Component { @@ -314,7 +314,7 @@ class SearchIndex extends Component { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} - isDisabled={hasNoSearchResults} + isDisabled={hasNoIndexer} onFilterSelect={onFilterSelect} /> diff --git a/frontend/src/Search/SearchIndexConnector.js b/frontend/src/Search/SearchIndexConnector.js index 78a9866b2..e3302e73c 100644 --- a/frontend/src/Search/SearchIndexConnector.js +++ b/frontend/src/Search/SearchIndexConnector.js @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import withScrollPosition from 'Components/withScrollPosition'; import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions'; -import { fetchDownloadClients } from 'Store/Actions/Settings/downloadClients'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector'; import SearchIndex from './SearchIndex'; @@ -56,20 +55,12 @@ function createMapDispatchToProps(dispatch, props) { dispatchClearReleases() { dispatch(clearReleases()); - }, - - dispatchFetchDownloadClients() { - dispatch(fetchDownloadClients()); } }; } class SearchIndexConnector extends Component { - componentDidMount() { - this.props.dispatchFetchDownloadClients(); - } - componentWillUnmount() { this.props.dispatchCancelFetchReleases(); this.props.dispatchClearReleases(); @@ -94,7 +85,6 @@ SearchIndexConnector.propTypes = { onBulkGrabPress: PropTypes.func.isRequired, dispatchCancelFetchReleases: PropTypes.func.isRequired, dispatchClearReleases: PropTypes.func.isRequired, - dispatchFetchDownloadClients: PropTypes.func.isRequired, items: PropTypes.arrayOf(PropTypes.object) }; diff --git a/frontend/src/Search/Table/SearchIndexItemConnector.js b/frontend/src/Search/Table/SearchIndexItemConnector.js index 4cc7fb20c..490214529 100644 --- a/frontend/src/Search/Table/SearchIndexItemConnector.js +++ b/frontend/src/Search/Table/SearchIndexItemConnector.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import { executeCommand } from 'Store/Actions/commandActions'; function createReleaseSelector() { return createSelector( @@ -36,6 +37,10 @@ function createMapStateToProps() { ); } +const mapDispatchToProps = { + dispatchExecuteCommand: executeCommand +}; + class SearchIndexItemConnector extends Component { // @@ -66,4 +71,4 @@ SearchIndexItemConnector.propTypes = { component: PropTypes.elementType.isRequired }; -export default connect(createMapStateToProps, null)(SearchIndexItemConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector); diff --git a/frontend/src/Search/Table/SearchIndexRow.css b/frontend/src/Search/Table/SearchIndexRow.css index b36ec4071..37b59a35d 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css +++ b/frontend/src/Search/Table/SearchIndexRow.css @@ -67,33 +67,3 @@ color: var(--textColor); } - -.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; -} diff --git a/frontend/src/Search/Table/SearchIndexRow.css.d.ts b/frontend/src/Search/Table/SearchIndexRow.css.d.ts index 7552b96f9..6d625f58a 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css.d.ts +++ b/frontend/src/Search/Table/SearchIndexRow.css.d.ts @@ -6,15 +6,12 @@ interface CssExports { 'category': string; 'cell': string; 'checkInput': string; - 'downloadIcon': string; 'downloadLink': string; 'externalLinks': string; 'files': string; 'grabs': string; 'indexer': string; 'indexerFlags': string; - 'interactiveIcon': string; - 'manualDownloadContent': string; 'peers': string; 'protocol': string; 'size': string; diff --git a/frontend/src/Search/Table/SearchIndexRow.js b/frontend/src/Search/Table/SearchIndexRow.js new file mode 100644 index 000000000..613605e02 --- /dev/null +++ b/frontend/src/Search/Table/SearchIndexRow.js @@ -0,0 +1,431 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; +import Popover from 'Components/Tooltip/Popover'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; +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 CategoryLabel from './CategoryLabel'; +import Peers from './Peers'; +import ReleaseLinks from './ReleaseLinks'; +import styles from './SearchIndexRow.css'; + +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 SearchIndexRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isConfirmGrabModalOpen: false + }; + } + + // + // Listeners + + onGrabPress = () => { + const { + guid, + indexerId, + onGrabPress + } = this.props; + + onGrabPress({ + guid, + indexerId + }); + }; + + onSavePress = () => { + const { + downloadUrl, + fileName, + onSavePress + } = this.props; + + onSavePress({ + downloadUrl, + fileName + }); + }; + + // + // Render + + render() { + const { + guid, + protocol, + downloadUrl, + magnetUrl, + categories, + age, + ageHours, + ageMinutes, + publishDate, + title, + infoUrl, + indexer, + size, + files, + grabs, + seeders, + leechers, + imdbId, + tmdbId, + tvdbId, + tvMazeId, + indexerFlags, + columns, + isGrabbing, + isGrabbed, + grabError, + longDateFormat, + timeFormat, + isSelected, + onSelectedChange + } = this.props; + + return ( + <> + { + columns.map((column) => { + const { + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (column.name === 'select') { + return ( + + ); + } + + if (column.name === 'protocol') { + return ( + + + + ); + } + + if (column.name === 'age') { + return ( + + {formatAge(age, ageHours, ageMinutes)} + + ); + } + + if (column.name === 'sortTitle') { + return ( + + +
+ {title} +
+ +
+ ); + } + + if (column.name === 'indexer') { + return ( + + {indexer} + + ); + } + + if (column.name === 'size') { + return ( + + {formatBytes(size)} + + ); + } + + if (column.name === 'files') { + return ( + + {files} + + ); + } + + if (column.name === 'grabs') { + return ( + + {grabs} + + ); + } + + if (column.name === 'peers') { + return ( + + { + protocol === 'torrent' && + + } + + ); + } + + if (column.name === 'category') { + return ( + + + + ); + } + + if (column.name === 'indexerFlags') { + return ( + + { + !!indexerFlags.length && + + } + title={translate('IndexerFlags')} + body={ +
    + { + indexerFlags.map((flag, index) => { + return ( +
  • + {titleCase(flag)} +
  • + ); + }) + } +
+ } + position={tooltipPositions.LEFT} + /> + } +
+ ); + } + + if (column.name === 'actions') { + return ( + + + + { + downloadUrl ? + : + null + } + + { + magnetUrl ? + : + null + } + + { + imdbId || tmdbId || tvdbId || tvMazeId ? ( + + } + title={translate('Links')} + body={ + + } + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> + ) : null + } + + ); + } + + return null; + }) + } + + ); + } +} + +SearchIndexRow.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, + fileName: 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, + imdbId: PropTypes.number, + tmdbId: PropTypes.number, + tvdbId: PropTypes.number, + tvMazeId: PropTypes.number, + indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onGrabPress: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + isGrabbing: PropTypes.bool.isRequired, + isGrabbed: PropTypes.bool.isRequired, + grabError: PropTypes.string, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +SearchIndexRow.defaultProps = { + isGrabbing: false, + isGrabbed: false +}; + +export default SearchIndexRow; diff --git a/frontend/src/Search/Table/SearchIndexRow.tsx b/frontend/src/Search/Table/SearchIndexRow.tsx deleted file mode 100644 index 1136a7f64..000000000 --- a/frontend/src/Search/Table/SearchIndexRow.tsx +++ /dev/null @@ -1,395 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; -import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; -import Column from 'Components/Table/Column'; -import Popover from 'Components/Tooltip/Popover'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import { IndexerCategory } from 'Indexer/Indexer'; -import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal'; -import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; -import { SelectStateInputProps } from 'typings/props'; -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 CategoryLabel from './CategoryLabel'; -import Peers from './Peers'; -import ReleaseLinks from './ReleaseLinks'; -import styles from './SearchIndexRow.css'; - -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 SearchIndexRowProps { - guid: string; - protocol: DownloadProtocol; - age: number; - ageHours: number; - ageMinutes: number; - publishDate: string; - title: string; - fileName: string; - infoUrl: string; - downloadUrl?: string; - magnetUrl?: string; - indexerId: number; - indexer: string; - categories: IndexerCategory[]; - size: number; - files?: number; - grabs?: number; - seeders?: number; - leechers?: number; - imdbId?: string; - tmdbId?: number; - tvdbId?: number; - tvMazeId?: number; - indexerFlags: string[]; - isGrabbing: boolean; - isGrabbed: boolean; - grabError?: string; - longDateFormat: string; - timeFormat: string; - columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; - onGrabPress(...args: unknown[]): void; - onSavePress(...args: unknown[]): void; -} - -function SearchIndexRow(props: SearchIndexRowProps) { - const { - guid, - indexerId, - protocol, - categories, - age, - ageHours, - ageMinutes, - publishDate, - title, - fileName, - infoUrl, - downloadUrl, - magnetUrl, - indexer, - size, - files, - grabs, - seeders, - leechers, - imdbId, - tmdbId, - tvdbId, - tvMazeId, - indexerFlags = [], - isGrabbing = false, - isGrabbed = false, - grabError, - longDateFormat, - timeFormat, - columns, - isSelected, - onSelectedChange, - onGrabPress, - onSavePress, - } = props; - - const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false); - - const { items: downloadClients } = useSelector( - createEnabledDownloadClientsSelector(protocol) - ); - - const onGrabPressWrapper = useCallback(() => { - onGrabPress({ - guid, - indexerId, - }); - }, [guid, indexerId, onGrabPress]); - - const onSavePressWrapper = useCallback(() => { - onSavePress({ - downloadUrl, - fileName, - }); - }, [downloadUrl, fileName, onSavePress]); - - const onOverridePress = useCallback(() => { - setIsOverrideModalOpen(true); - }, [setIsOverrideModalOpen]); - - const onOverrideModalClose = useCallback(() => { - setIsOverrideModalOpen(false); - }, [setIsOverrideModalOpen]); - - return ( - <> - {columns.map((column) => { - const { name, isVisible } = column; - - if (!isVisible) { - return null; - } - - if (name === 'select') { - return ( - - ); - } - - if (name === 'protocol') { - return ( - - - - ); - } - - if (name === 'age') { - return ( - - {formatAge(age, ageHours, ageMinutes)} - - ); - } - - if (name === 'sortTitle') { - return ( - - -
{title}
- -
- ); - } - - if (name === 'indexer') { - return ( - - {indexer} - - ); - } - - if (name === 'size') { - return ( - - {formatBytes(size)} - - ); - } - - if (name === 'files') { - return ( - - {files} - - ); - } - - if (name === 'grabs') { - return ( - - {grabs} - - ); - } - - if (name === 'peers') { - return ( - - {protocol === 'torrent' && ( - - )} - - ); - } - - if (name === 'category') { - return ( - - - - ); - } - - if (name === 'indexerFlags') { - return ( - - {!!indexerFlags.length && ( - } - title={translate('IndexerFlags')} - body={ -
    - {indexerFlags.map((flag, index) => { - return
  • {titleCase(flag)}
  • ; - })} -
- } - position={tooltipPositions.LEFT} - /> - )} -
- ); - } - - if (name === 'actions') { - return ( - - - - {downloadClients.length > 1 ? ( - -
- - - -
- - ) : null} - - {downloadUrl ? ( - - ) : null} - - {magnetUrl ? ( - - ) : null} - - {imdbId || tmdbId || tvdbId || tvMazeId ? ( - - } - title={translate('Links')} - body={ - - } - position={tooltipPositions.TOP} - /> - ) : null} -
- ); - } - - return null; - })} - - - - ); -} - -export default SearchIndexRow; diff --git a/frontend/src/Settings/Applications/ApplicationSettings.tsx b/frontend/src/Settings/Applications/ApplicationSettings.tsx index 7fc4b1d7b..c35d55e2d 100644 --- a/frontend/src/Settings/Applications/ApplicationSettings.tsx +++ b/frontend/src/Settings/Applications/ApplicationSettings.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { Fragment, useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; import { APP_INDEXER_SYNC } from 'Commands/commandNames'; @@ -56,7 +56,7 @@ function ApplicationSettings() { // @ts-ignore showSave={false} additionalButtons={ - <> + - + } /> diff --git a/frontend/src/Settings/Applications/Applications/Application.js b/frontend/src/Settings/Applications/Applications/Application.js index 086d39ee1..610cc344d 100644 --- a/frontend/src/Settings/Applications/Applications/Application.js +++ b/frontend/src/Settings/Applications/Applications/Application.js @@ -57,7 +57,6 @@ class Application extends Component { const { id, name, - enable, syncLevel, fields, tags, @@ -78,7 +77,7 @@ class Application extends Component {
{ - enable && applicationUrl ? + applicationUrl ? { return { diff --git a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx index 57e88a4fe..10e73b52a 100644 --- a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx +++ b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx @@ -30,7 +30,7 @@ const syncLevelOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: ApplicationSyncLevel.Disabled, diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx index bb81729f3..e2c36529c 100644 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx +++ b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx @@ -213,9 +213,9 @@ function ManageApplicationsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} + onSelectAllChange={onSelectAllChange} sortKey={sortKey} sortDirection={sortDirection} - onSelectAllChange={onSelectAllChange} onSortPress={onSortPress} > @@ -268,9 +268,9 @@ function ManageApplicationsModalContent( - -
- {translate('ProwlarrDownloadClientsAlert')} -
-
- {translate('ProwlarrDownloadClientsInAppOnlyAlert')} -
-
- -
- -
- { - items.map((item) => { - return ( - - ); - }) - } - - -
- + +
+ { + items.map((item) => { + return ( + -
- -
+ ); + }) + } - + +
+ +
+
+
- -
-
-
+ + + + + ); } } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index 4f6833fcb..9cba9c1cc 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByProp('name')), + createSortedSectionSelector('settings.downloadClients', sortByName), (downloadClients) => downloadClients ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index c57432710..b4dd3c1e9 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -17,7 +17,6 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { icons, inputTypes, kinds } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import AddCategoryModalConnector from './Categories/AddCategoryModalConnector'; import Category from './Categories/Category'; @@ -62,7 +61,6 @@ class EditDownloadClientModalContent extends Component { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteDownloadClientPress, onConfirmDeleteCategory, ...otherProps @@ -221,12 +219,6 @@ class EditDownloadClientModalContent extends Component { } - - { - this.props.toggleAdvancedSettings(); - }; - onConfirmDeleteCategory = (id) => { this.props.deleteDownloadClientCategory({ id }); }; @@ -94,7 +81,6 @@ class EditDownloadClientModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} onConfirmDeleteCategory={this.onConfirmDeleteCategory} @@ -116,7 +102,6 @@ EditDownloadClientModalContentConnector.propTypes = { setDownloadClientFieldValue: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired, testDownloadClient: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index d18e694c9..a2e0f89c6 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -30,7 +30,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index fa82d61b9..4d459d71d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -186,9 +186,9 @@ function ManageDownloadClientsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} + onSelectAllChange={onSelectAllChange} sortKey={sortKey} sortDirection={sortDirection} - onSelectAllChange={onSelectAllChange} onSortPress={onSortPress} > @@ -233,9 +233,9 @@ function ManageDownloadClientsModalContent( diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.js index 61a259258..540e29b01 100644 --- a/frontend/src/Settings/General/LoggingSettings.js +++ b/frontend/src/Settings/General/LoggingSettings.js @@ -15,14 +15,12 @@ const logLevelOptions = [ function LoggingSettings(props) { const { - advancedSettings, settings, onInputChange } = props; const { - logLevel, - logSizeLimit + logLevel } = settings; return ( @@ -39,30 +37,11 @@ function LoggingSettings(props) { {...logLevel} /> - - - {translate('LogSizeLimit')} - - - ); } LoggingSettings.propTypes = { - advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index 9cf1b7932..3bf8d43b6 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -12,6 +12,7 @@ function UpdateSettings(props) { const { advancedSettings, settings, + isWindows, packageUpdateMechanism, onInputChange } = props; @@ -37,10 +38,10 @@ function UpdateSettings(props) { value: titleCase(packageUpdateMechanism) }); } else { - updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') }); + updateOptions.push({ key: 'builtIn', value: 'Built-In' }); } - updateOptions.push({ key: 'script', value: translate('Script') }); + updateOptions.push({ key: 'script', value: 'Script' }); return (
@@ -61,58 +62,61 @@ function UpdateSettings(props) { /> -
- - {translate('Automatic')} + { + !isWindows && +
+ + {translate('Automatic')} - - + + - - {translate('Mechanism')} - - - - - { - updateMechanism.value === 'script' && - {translate('ScriptPath')} + {translate('Mechanism')} - } -
+ + { + updateMechanism.value === 'script' && + + {translate('ScriptPath')} + + + + } +
+ }
); } diff --git a/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js b/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js index 59e10422b..8579c2a2f 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js @@ -14,7 +14,6 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import styles from './EditIndexerProxyModalContent.css'; @@ -32,7 +31,6 @@ function EditIndexerProxyModalContent(props) { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteIndexerProxyPress, ...otherProps } = props; @@ -132,12 +130,6 @@ function EditIndexerProxyModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -76,7 +65,6 @@ class EditIndexerProxyModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -94,7 +82,6 @@ EditIndexerProxyModalContentConnector.propTypes = { setIndexerProxyFieldValue: PropTypes.func.isRequired, saveIndexerProxy: PropTypes.func.isRequired, testIndexerProxy: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js index 0d2acae87..9d2188a7c 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js @@ -5,13 +5,13 @@ import { createSelector } from 'reselect'; import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import IndexerProxies from './IndexerProxies'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.indexerProxies', sortByProp('name')), - createSortedSectionSelector('indexers', sortByProp('name')), + createSortedSectionSelector('settings.indexerProxies', sortByName), + createSortedSectionSelector('indexers', sortByName), createTagsSelector(), (indexerProxies, indexers, tagList) => { return { diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js index ed00d96e6..60e368617 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -14,7 +14,6 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import NotificationEventItems from './NotificationEventItems'; import styles from './EditNotificationModalContent.css'; @@ -33,7 +32,6 @@ function EditNotificationModalContent(props) { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteNotificationPress, ...otherProps } = props; @@ -138,12 +136,6 @@ function EditNotificationModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -76,7 +65,6 @@ class EditNotificationModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -94,7 +82,6 @@ EditNotificationModalContentConnector.propTypes = { setNotificationFieldValue: PropTypes.func.isRequired, saveNotification: PropTypes.func.isRequired, testNotification: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index 6351c6f8a..b306f742a 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByProp('name')), + createSortedSectionSelector('settings.notifications', sortByName), createTagsSelector(), (notifications, tagList) => { return { diff --git a/frontend/src/Settings/PendingChangesModal.js b/frontend/src/Settings/PendingChangesModal.js index 213445c65..4cb83e8f6 100644 --- a/frontend/src/Settings/PendingChangesModal.js +++ b/frontend/src/Settings/PendingChangesModal.js @@ -15,17 +15,12 @@ function PendingChangesModal(props) { isOpen, onConfirm, onCancel, - bindShortcut, - unbindShortcut + bindShortcut } = props; useEffect(() => { - if (isOpen) { - bindShortcut('enter', onConfirm); - - return () => unbindShortcut('enter', onConfirm); - } - }, [bindShortcut, unbindShortcut, isOpen, onConfirm]); + bindShortcut('enter', onConfirm); + }, [bindShortcut, onConfirm]); return ( - {translate('Rss')} + {translate('RSS')} } diff --git a/frontend/src/Settings/Profiles/App/AppProfilesConnector.js b/frontend/src/Settings/Profiles/App/AppProfilesConnector.js index 02bf845df..a150655a6 100644 --- a/frontend/src/Settings/Profiles/App/AppProfilesConnector.js +++ b/frontend/src/Settings/Profiles/App/AppProfilesConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import AppProfiles from './AppProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.appProfiles', sortByProp('name')), + createSortedSectionSelector('settings.appProfiles', sortByName), (appProfiles) => appProfiles ); } diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 1f3de2034..4f311e984 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -3,14 +3,12 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions'; -import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import { fetchTagDetails } from 'Store/Actions/tagActions'; import Tags from './Tags'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('tags', sortByProp('label')), + (state) => state.tags, (tags) => { const isFetching = tags.isFetching || tags.details.isFetching; const error = tags.error || tags.details.error; @@ -27,7 +25,6 @@ function createMapStateToProps() { } const mapDispatchToProps = { - dispatchFetchTags: fetchTags, dispatchFetchTagDetails: fetchTagDetails, dispatchFetchNotifications: fetchNotifications, dispatchFetchIndexerProxies: fetchIndexerProxies, @@ -41,14 +38,12 @@ class MetadatasConnector extends Component { componentDidMount() { const { - dispatchFetchTags, dispatchFetchTagDetails, dispatchFetchNotifications, dispatchFetchIndexerProxies, dispatchFetchApplications } = this.props; - dispatchFetchTags(); dispatchFetchTagDetails(); dispatchFetchNotifications(); dispatchFetchIndexerProxies(); @@ -68,7 +63,6 @@ class MetadatasConnector extends Component { } MetadatasConnector.propTypes = { - dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchNotifications: PropTypes.func.isRequired, dispatchFetchIndexerProxies: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index d156f4ff3..caa013db3 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -21,19 +21,19 @@ export const firstDayOfWeekOptions = [ ]; export const weekColumnOptions = [ - { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' }, - { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' }, - { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' }, - { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' } + { key: 'ddd M/D', value: 'Tue 3/25' }, + { key: 'ddd MM/DD', value: 'Tue 03/25' }, + { key: 'ddd D/M', value: 'Tue 25/3' }, + { key: 'ddd DD/MM', value: 'Tue 25/03' } ]; const shortDateFormatOptions = [ - { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' }, - { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' }, - { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' }, - { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' }, - { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' }, - { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' } + { key: 'MMM D YYYY', value: 'Mar 25 2014' }, + { key: 'DD MMM YYYY', value: '25 Mar 2014' }, + { key: 'MM/D/YYYY', value: '03/25/2014' }, + { key: 'MM/DD/YYYY', value: '03/25/2014' }, + { key: 'DD/MM/YYYY', value: '25/03/2014' }, + { key: 'YYYY-MM-DD', value: '2014-03-25' } ]; const longDateFormatOptions = [ diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index e35157dbd..ca26883fb 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -1,11 +1,8 @@ -import $ from 'jquery'; -import _ from 'lodash'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getProviderState from 'Utilities/State/getProviderState'; import { set } from '../baseActions'; const abortCurrentRequests = {}; -let lastTestData = null; export function createCancelTestProviderHandler(section) { return function(getState, payload, dispatch) { @@ -20,25 +17,10 @@ function createTestProviderHandler(section, url) { return function(getState, payload, dispatch) { dispatch(set({ section, isTesting: true })); - const { - queryParams = {}, - ...otherPayload - } = payload; - - const testData = getProviderState({ ...otherPayload }, getState, section); - const params = { ...queryParams }; - - // If the user is re-testing the same provider without changes - // force it to be tested. - - if (_.isEqual(testData, lastTestData)) { - params.forceTest = true; - } - - lastTestData = testData; + const testData = getProviderState(payload, getState, section); const ajaxOptions = { - url: `${url}/test?${$.param(params, true)}`, + url: `${url}/test`, method: 'POST', contentType: 'application/json', dataType: 'json', @@ -50,8 +32,6 @@ function createTestProviderHandler(section, url) { abortCurrentRequests[section] = abortRequest; request.done((data) => { - lastTestData = null; - dispatch(set({ section, isTesting: false, diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index c324fe227..95ea992b1 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -82,12 +82,6 @@ export const defaultState = { isSortable: false, isVisible: false }, - { - name: 'host', - label: () => translate('Host'), - isSortable: false, - isVisible: false - }, { name: 'elapsedTime', label: () => translate('ElapsedTime'), diff --git a/frontend/src/Store/Actions/indexerActions.js b/frontend/src/Store/Actions/indexerActions.js index e11051c2f..5a4261c29 100644 --- a/frontend/src/Store/Actions/indexerActions.js +++ b/frontend/src/Store/Actions/indexerActions.js @@ -3,13 +3,9 @@ import { createAction } from 'redux-actions'; import { filterTypePredicates, sortDirections } from 'Helpers/Props'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; -import createSaveProviderHandler, { - createCancelSaveProviderHandler -} from 'Store/Actions/Creators/createSaveProviderHandler'; +import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; -import createTestProviderHandler, { - createCancelTestProviderHandler -} from 'Store/Actions/Creators/createTestProviderHandler'; +import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk, handleThunks } from 'Store/thunks'; @@ -20,7 +16,6 @@ import translate from 'Utilities/String/translate'; import createBulkEditItemHandler from './Creators/createBulkEditItemHandler'; import createBulkRemoveItemHandler from './Creators/createBulkRemoveItemHandler'; import createHandleActions from './Creators/createHandleActions'; -import createClearReducer from './Creators/Reducers/createClearReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; // @@ -100,42 +95,11 @@ export const filterPredicates = { }; export const sortPredicates = { - status: function({ enable, redirect }) { - let result = 0; + vipExpiration: function(item) { + const vipExpiration = + item.fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; - if (redirect) { - result++; - } - - if (enable) { - result += 2; - } - - return result; - }, - - vipExpiration: function({ fields = [] }) { - return fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; - }, - - minimumSeeders: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.appMinimumSeeders')?.value ?? undefined; - }, - - seedRatio: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')?.value ?? undefined; - }, - - seedTime: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.seedTime')?.value ?? undefined; - }, - - packSeedTime: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')?.value ?? undefined; - }, - - preferMagnetUrl: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')?.value ?? undefined; + return vipExpiration; } }; @@ -146,7 +110,6 @@ export const FETCH_INDEXERS = 'indexers/fetchIndexers'; export const FETCH_INDEXER_SCHEMA = 'indexers/fetchIndexerSchema'; export const SELECT_INDEXER_SCHEMA = 'indexers/selectIndexerSchema'; export const SET_INDEXER_SCHEMA_SORT = 'indexers/setIndexerSchemaSort'; -export const CLEAR_INDEXER_SCHEMA = 'indexers/clearIndexerSchema'; export const CLONE_INDEXER = 'indexers/cloneIndexer'; export const SET_INDEXER_VALUE = 'indexers/setIndexerValue'; export const SET_INDEXER_FIELD_VALUE = 'indexers/setIndexerFieldValue'; @@ -166,7 +129,6 @@ export const fetchIndexers = createThunk(FETCH_INDEXERS); export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA); export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA); export const setIndexerSchemaSort = createAction(SET_INDEXER_SCHEMA_SORT); -export const clearIndexerSchema = createAction(CLEAR_INDEXER_SCHEMA); export const cloneIndexer = createAction(CLONE_INDEXER); export const saveIndexer = createThunk(SAVE_INDEXER); @@ -252,8 +214,6 @@ export const reducers = createHandleActions({ }); }, - [CLEAR_INDEXER_SCHEMA]: createClearReducer(schemaSection, defaultState), - [CLONE_INDEXER]: function(state, { payload }) { const id = payload.id; const newState = getSectionState(state, section); @@ -265,13 +225,7 @@ export const reducers = createHandleActions({ delete selectedSchema.name; selectedSchema.fields = selectedSchema.fields.map((field) => { - const newField = { ...field }; - - if (newField.privacy === 'apiKey' || newField.privacy === 'password') { - newField.value = ''; - } - - return newField; + return { ...field }; }); newState.selectedSchema = selectedSchema; diff --git a/frontend/src/Store/Actions/indexerIndexActions.js b/frontend/src/Store/Actions/indexerIndexActions.js index a002d9b41..fa4bc3a15 100644 --- a/frontend/src/Store/Actions/indexerIndexActions.js +++ b/frontend/src/Store/Actions/indexerIndexActions.js @@ -116,12 +116,6 @@ export const defaultState = { isSortable: true, isVisible: false }, - { - name: 'preferMagnetUrl', - label: () => translate('PreferMagnetUrl'), - isSortable: true, - isVisible: false - }, { name: 'tags', label: () => translate('Tags'), diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index fd2fe441b..bdb3ce2df 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -401,16 +401,7 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ [CLEAR_RELEASES]: (state) => { - const { - sortKey, - sortDirection, - customFilters, - selectedFilterKey, - columns, - ...otherDefaultState - } = defaultState; - - return Object.assign({}, state, otherDefaultState); + return Object.assign({}, state, defaultState); }, [UPDATE_RELEASE]: (state, { payload }) => { diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index 75d2595cf..92360b589 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -110,6 +110,7 @@ export const defaultState = { { name: 'actions', columnLabel: () => translate('Actions'), + isSortable: true, isVisible: true, isModifiable: false } diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.ts b/frontend/src/Store/Selectors/createDimensionsSelector.js similarity index 69% rename from frontend/src/Store/Selectors/createDimensionsSelector.ts rename to frontend/src/Store/Selectors/createDimensionsSelector.js index b9602cb02..ce26b2e2c 100644 --- a/frontend/src/Store/Selectors/createDimensionsSelector.ts +++ b/frontend/src/Store/Selectors/createDimensionsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createDimensionsSelector() { return createSelector( - (state: AppState) => state.app.dimensions, + (state) => state.app.dimensions, (dimensions) => { return dimensions; } diff --git a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts deleted file mode 100644 index 3a581587b..000000000 --- a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSelector } from 'reselect'; -import { DownloadClientAppState } from 'App/State/SettingsAppState'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import DownloadClient from 'typings/DownloadClient'; -import sortByProp from 'Utilities/Array/sortByProp'; - -export default function createEnabledDownloadClientsSelector( - protocol: DownloadProtocol -) { - return createSelector( - createSortedSectionSelector( - 'settings.downloadClients', - sortByProp('name') - ), - (downloadClients: DownloadClientAppState) => { - const { isFetching, isPopulated, error, items } = downloadClients; - - const clients = items.filter( - (item) => item.protocol === protocol && item.enable - ); - - return { isFetching, isPopulated, error, items: clients }; - } - ); -} diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.ts b/frontend/src/Store/Selectors/createSortedSectionSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.ts rename to frontend/src/Store/Selectors/createSortedSectionSelector.js index abee01f75..331d890c9 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.js @@ -1,18 +1,14 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector( - section: string, - comparer: (a: T, b: T) => number -) { +function createSortedSectionSelector(section, comparer) { return createSelector( (state) => state, (state) => { const sectionState = getSectionState(state, section, true); - return { ...sectionState, - items: [...sectionState.items].sort(comparer), + items: [...sectionState.items].sort(comparer) }; } ); diff --git a/frontend/src/Styles/Themes/dark.js b/frontend/src/Styles/Themes/dark.js index a7cbb6de0..79911686f 100644 --- a/frontend/src/Styles/Themes/dark.js +++ b/frontend/src/Styles/Themes/dark.js @@ -188,7 +188,7 @@ module.exports = { // Charts chartBackgroundColor: '#262626', - failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','), - chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','), - chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',') + failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], + chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], + chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] }; diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js index 4dec39164..d93c5dd8c 100644 --- a/frontend/src/Styles/Themes/index.js +++ b/frontend/src/Styles/Themes/index.js @@ -2,7 +2,7 @@ import * as dark from './dark'; import * as light from './light'; const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const auto = defaultDark ? dark : light; +const auto = defaultDark ? { ...dark } : { ...light }; export default { auto, diff --git a/frontend/src/Styles/Themes/light.js b/frontend/src/Styles/Themes/light.js index f88070a0f..8a6d123b2 100644 --- a/frontend/src/Styles/Themes/light.js +++ b/frontend/src/Styles/Themes/light.js @@ -188,7 +188,7 @@ module.exports = { // Charts chartBackgroundColor: '#fff', - failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','), - chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','), - chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',') + failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], + chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], + chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] }; diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js index def48f28e..3b0077c5a 100644 --- a/frontend/src/Styles/Variables/fonts.js +++ b/frontend/src/Styles/Variables/fonts.js @@ -2,6 +2,7 @@ module.exports = { // Families defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', + passwordFamily: 'text-security-disc', // Sizes extraSmallFontSize: '11px', diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js index 39f7f1123..5dfc7f940 100644 --- a/frontend/src/System/Backup/BackupRow.js +++ b/frontend/src/System/Backup/BackupRow.js @@ -116,7 +116,6 @@ class BackupRow extends Component { @@ -139,9 +138,7 @@ class BackupRow extends Component { isOpen={isConfirmDeleteModalOpen} kind={kinds.DANGER} title={translate('DeleteBackup')} - message={translate('DeleteBackupMessageText', { - name - })} + message={translate('DeleteBackupMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeletePress} onCancel={this.onConfirmDeleteModalClose} diff --git a/frontend/src/System/Backup/Backups.js b/frontend/src/System/Backup/Backups.js index ede2f97f6..8f7a5b0a5 100644 --- a/frontend/src/System/Backup/Backups.js +++ b/frontend/src/System/Backup/Backups.js @@ -109,7 +109,7 @@ class Backups extends Component { { !isFetching && !!error && - {translate('BackupsLoadError')} + {translate('UnableToLoadBackups')} } diff --git a/frontend/src/System/Backup/RestoreBackupModalContent.js b/frontend/src/System/Backup/RestoreBackupModalContent.js index 9b5daa9f4..150c46ad6 100644 --- a/frontend/src/System/Backup/RestoreBackupModalContent.js +++ b/frontend/src/System/Backup/RestoreBackupModalContent.js @@ -14,7 +14,7 @@ import styles from './RestoreBackupModalContent.css'; function getErrorMessage(error) { if (!error || !error.responseJSON || !error.responseJSON.message) { - return translate('ErrorRestoringBackup'); + return 'Error restoring backup'; } return error.responseJSON.message; @@ -146,9 +146,7 @@ class RestoreBackupModalContent extends Component { { - !!id && translate('WouldYouLikeToRestoreBackup', { - name - }) + !!id && `Would you like to restore the backup '${name}'?` } { @@ -205,7 +203,7 @@ class RestoreBackupModalContent extends Component {
- {translate('RestartReloadNote')} + Note: Prowlarr will automatically restart and reload the UI during the restore process.