mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-18 21:55:12 -04:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55fa1ec637 | |||
| b27a3d8272 | |||
| 089d450b46 | |||
| 358ac7434d | |||
| 9cd505fd8a | |||
| 20ac2687df | |||
| 9f075c09a2 | |||
| 3793538ba4 | |||
| 4c4b16d234 | |||
| f5790bec2e | |||
| 6c0d08de56 | |||
| ba344756b1 | |||
| dfda86aca3 | |||
| df6f83ed69 | |||
| 218d92a1ac | |||
| df2b529d01 | |||
| 0ef42dbb4d | |||
| 1a428197b2 | |||
| 09d7983845 | |||
| 6e01f3187a | |||
| 468436b9f7 | |||
| 76c288a6e4 | |||
| f95f67a7ca | |||
| 11864247eb | |||
| 74509ea7c9 | |||
| 948fe0a6dc | |||
| a4257cbcde | |||
| 2929c3c898 | |||
| 2c5f2187c8 | |||
| 401ef88971 | |||
| 4fb3754048 | |||
| 596efe8fb0 | |||
| 076a4f2574 | |||
| 9561371a47 | |||
| 16254cf5f9 | |||
| 649a03e5a0 | |||
| dd21d9b521 | |||
| 68b895d2ad | |||
| 634016ae1b | |||
| 83c6751847 | |||
| 04bb0c51b1 | |||
| d2e9621de9 | |||
| cb673ddc42 | |||
| 440618f2b6 | |||
| ae79d45664 | |||
| 1877ccb513 | |||
| b3098f2e4c | |||
| 3e0af062c1 | |||
| 858f85c50d | |||
| 938848be65 | |||
| 615f5899cc | |||
| 5a6b1313e8 | |||
| ab7debb34b | |||
| eee21de795 | |||
| 15fabbe7d0 | |||
| 6aef48c6e7 | |||
| b29bc923fc | |||
| b223e9b0cc | |||
| 77a982a7da | |||
| ab3dc765b4 | |||
| 0261201360 | |||
| 1da3954879 | |||
| 742dd5ff54 | |||
| a85406e3b7 | |||
| 73cdaf3d44 | |||
| e26fa2dbf4 | |||
| 64be68a22d | |||
| 478a185968 | |||
| 4ff5d11a03 | |||
| 6000952b76 | |||
| 5fee2c4cd9 | |||
| 21d553cf1b | |||
| 782b2d3761 | |||
| e1da3eee80 | |||
| 09af2da6b9 | |||
| e3e9094d42 | |||
| 94634234ff | |||
| a48d6029d9 | |||
| 9cc150b105 | |||
| 6a97d99876 | |||
| c957168040 | |||
| 61bc35b3fa | |||
| a84210c452 | |||
| 8af6ea1d8f | |||
| 1a894ac583 | |||
| 4f6e05414c | |||
| 5096a088d4 | |||
| 6581bddba3 | |||
| 292af28d42 | |||
| 37a6d03d52 | |||
| fe35d450f0 | |||
| 6a9e27bc06 | |||
| a989bf82ea | |||
| ccc8d8002f | |||
| eaaf8db486 | |||
| c32fa7a84b | |||
| 57e21a78ee | |||
| 9cdf5d18d8 | |||
| 41b0a1211b | |||
| 1b8f09f2ce | |||
| 2f85de6b69 | |||
| b2ef9d5b0a | |||
| c80262d75b | |||
| 2a312d93ec | |||
| e09df2fff3 | |||
| f0c7d13b20 | |||
| 4dac60bef9 | |||
| 5aefb46790 | |||
| 41b043e551 | |||
| 5447fad1fc | |||
| 6a1e01abbd | |||
| 2803ad5ba0 | |||
| 8fa8a13036 | |||
| 41ce79ccce | |||
| 14ae062db2 | |||
| d55a38da4a | |||
| ab289cfc86 | |||
| 12671e9905 | |||
| a33023b8c6 | |||
| a3e134ce0b | |||
| ee7c821cab | |||
| ee4cf93aee | |||
| 2cacfba81f | |||
| 02e420580e | |||
| d99398d3f8 | |||
| 9ea8335aa0 | |||
| 52a91a50b2 | |||
| 680bf46e25 | |||
| d279c97f15 | |||
| 7d5d338c8e | |||
| 721ae1cac0 | |||
| 3881c9d753 | |||
| 131b344119 | |||
| d226e52881 | |||
| 583815b4f7 |
@@ -0,0 +1,13 @@
|
|||||||
|
// 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": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
||||||
+20
-8
@@ -1,19 +1,31 @@
|
|||||||
'Area: API':
|
'Area: API':
|
||||||
- src/Prowlarr.Api.V1/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/Prowlarr.Api.V1/**/*
|
||||||
|
|
||||||
'Area: Db-migration':
|
'Area: Db-migration':
|
||||||
- src/NzbDrone.Core/Datastore/Migration/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Datastore/Migration/*
|
||||||
|
|
||||||
'Area: Download Clients':
|
'Area: Download Clients':
|
||||||
- src/NzbDrone.Core/Download/Clients/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Download/Clients/**/*
|
||||||
|
|
||||||
'Area: Indexer':
|
'Area: Indexer':
|
||||||
- src/NzbDrone.Core/Indexers/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Indexers/**/*
|
||||||
|
|
||||||
'Area: Notifications':
|
'Area: Notifications':
|
||||||
- src/NzbDrone.Core/Notifications/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Notifications/**/*
|
||||||
|
|
||||||
'Area: UI':
|
'Area: UI':
|
||||||
- frontend/**/*
|
- changed-files:
|
||||||
- package.json
|
- any-glob-to-any-file:
|
||||||
- yarn.lock
|
- frontend/**/*
|
||||||
|
- package.json
|
||||||
|
- yarn.lock
|
||||||
@@ -9,4 +9,4 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v5
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v4
|
- uses: dessant/lock-threads@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ coverage*.xml
|
|||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
# VS outout folders
|
# VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+44
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+6
-6
@@ -9,13 +9,13 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '1.14.3'
|
majorVersion: '1.19.0'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.417'
|
dotnetVersion: '6.0.421'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
@@ -166,10 +166,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1075,10 +1075,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
@@ -21,17 +25,23 @@ slnFile=src/Prowlarr.sln
|
|||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
application=Prowlarr.Console.dll
|
||||||
|
else
|
||||||
|
application=Prowlarr.dll
|
||||||
|
fi
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
|
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||||
|
|
||||||
sleep 30
|
sleep 45
|
||||||
|
|
||||||
kill %1
|
kill %1
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ function App({ store, history }) {
|
|||||||
<DocumentTitle title={window.Prowlarr.instanceName}>
|
<DocumentTitle title={window.Prowlarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme>
|
<ApplyTheme />
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes app={App} />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
</ApplyTheme>
|
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import themes from 'Styles/Themes';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
|
||||||
(
|
|
||||||
theme
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
theme
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplyTheme({ theme, children }) {
|
|
||||||
// Update the CSS Variables
|
|
||||||
|
|
||||||
const updateCSSVariables = useCallback(() => {
|
|
||||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
|
||||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
|
||||||
|
|
||||||
// Loop through each array key and set the CSS Variables
|
|
||||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
|
||||||
// Based on our snippet from MDN
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
`--${cssVariableKey}`,
|
|
||||||
arrayOfVariableValues[index]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// On Component Mount and Component Update
|
|
||||||
useEffect(() => {
|
|
||||||
updateCSSVariables(theme);
|
|
||||||
}, [updateCSSVariables, theme]);
|
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyTheme.propTypes = {
|
|
||||||
theme: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
|
||||||
|
interface ApplyThemeProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createThemeSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||||
|
(theme) => {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||||
|
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 <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplyTheme;
|
||||||
@@ -42,7 +42,16 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
indexerHistory: IndexerHistoryAppState;
|
indexerHistory: IndexerHistoryAppState;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
descriptionClassName,
|
descriptionClassName,
|
||||||
title,
|
title,
|
||||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
<DescriptionListItemTitle
|
<DescriptionListItemTitle
|
||||||
className={titleClassName}
|
className={titleClassName}
|
||||||
>
|
>
|
||||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DescriptionListItem.propTypes = {
|
DescriptionListItem.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
titleClassName: PropTypes.string,
|
titleClassName: PropTypes.string,
|
||||||
descriptionClassName: PropTypes.string,
|
descriptionClassName: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
.validationFailures {
|
.validationFailures {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'details': string;
|
||||||
'validationFailures': string;
|
'validationFailures': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import { kinds } from 'Helpers/Props';
|
import Icon from 'Components/Icon';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import styles from './Form.css';
|
import styles from './Form.css';
|
||||||
|
|
||||||
function Form(props) {
|
function Form(props) {
|
||||||
@@ -26,6 +28,18 @@ function Form(props) {
|
|||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{error.errorMessage}
|
{error.errorMessage}
|
||||||
|
|
||||||
|
{
|
||||||
|
error.detailedDescription ?
|
||||||
|
<Tooltip
|
||||||
|
className={styles.details}
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={error.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -39,6 +53,18 @@ function Form(props) {
|
|||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
{warning.errorMessage}
|
{warning.errorMessage}
|
||||||
|
|
||||||
|
{
|
||||||
|
warning.detailedDescription ?
|
||||||
|
<Tooltip
|
||||||
|
className={styles.details}
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={warning.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ FormInputGroup.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
isFloat: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
.input {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
font-family: $passwordFamily;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './PasswordInput.css';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
function onCopy(e) {
|
function onCopy(e) {
|
||||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
{...props}
|
{...props}
|
||||||
|
type="password"
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
className: PropTypes.string.isRequired
|
...TextInput.props
|
||||||
};
|
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
|
||||||
className: styles.input
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordInput;
|
export default PasswordInput;
|
||||||
|
|||||||
@@ -25,14 +25,3 @@
|
|||||||
font-family: 'Ubuntu Mono';
|
font-family: 'Ubuntu Mono';
|
||||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* text-security-disc
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-family: 'text-security-disc';
|
|
||||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
enum DownloadProtocol {
|
||||||
|
Unknown = 'unknown',
|
||||||
|
Usenet = 'usenet',
|
||||||
|
Torrent = 'torrent',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadProtocol;
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
faChevronCircleRight as fasChevronCircleRight,
|
faChevronCircleRight as fasChevronCircleRight,
|
||||||
faChevronCircleUp as fasChevronCircleUp,
|
faChevronCircleUp as fasChevronCircleUp,
|
||||||
faCircle as fasCircle,
|
faCircle as fasCircle,
|
||||||
|
faCircleDown as fasCircleDown,
|
||||||
faCloud as fasCloud,
|
faCloud as fasCloud,
|
||||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||||
faCog as fasCog,
|
faCog as fasCog,
|
||||||
@@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
|||||||
export const CHECK_CIRCLE = fasCheckCircle;
|
export const CHECK_CIRCLE = fasCheckCircle;
|
||||||
export const CHECK_SQUARE = fasSquareCheck;
|
export const CHECK_SQUARE = fasSquareCheck;
|
||||||
export const CIRCLE = fasCircle;
|
export const CIRCLE = fasCircle;
|
||||||
|
export const CIRCLE_DOWN = fasCircleDown;
|
||||||
export const CIRCLE_OUTLINE = farCircle;
|
export const CIRCLE_OUTLINE = farCircle;
|
||||||
export const CLEAR = fasTrashAlt;
|
export const CLEAR = fasTrashAlt;
|
||||||
export const CLIPBOARD = fasCopy;
|
export const CLIPBOARD = fasCopy;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function HistoryDetails(props) {
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
source,
|
source,
|
||||||
|
host,
|
||||||
url
|
url
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
@@ -86,6 +87,15 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Host')}
|
||||||
|
data={host}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data ?
|
data ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|||||||
@@ -331,6 +331,21 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'host') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexer}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
data.host ?
|
||||||
|
data.host :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'elapsedTime') {
|
if (name === 'elapsedTime') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ interface SavePayload {
|
|||||||
seedRatio?: number;
|
seedRatio?: number;
|
||||||
seedTime?: number;
|
seedTime?: number;
|
||||||
packSeedTime?: number;
|
packSeedTime?: number;
|
||||||
rejectBlocklistedTorrentHashesWhileGrabbing?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditIndexerModalContentProps {
|
interface EditIndexerModalContentProps {
|
||||||
@@ -66,10 +65,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
|
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [
|
|
||||||
rejectBlocklistedTorrentHashesWhileGrabbing,
|
|
||||||
setRejectBlocklistedTorrentHashesWhileGrabbing,
|
|
||||||
] = useState(NO_CHANGE);
|
|
||||||
|
|
||||||
const save = useCallback(() => {
|
const save = useCallback(() => {
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
@@ -110,12 +105,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
payload.packSeedTime = packSeedTime as number;
|
payload.packSeedTime = packSeedTime as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rejectBlocklistedTorrentHashesWhileGrabbing !== NO_CHANGE) {
|
|
||||||
hasChanges = true;
|
|
||||||
payload.rejectBlocklistedTorrentHashesWhileGrabbing =
|
|
||||||
rejectBlocklistedTorrentHashesWhileGrabbing === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
onSavePress(payload);
|
onSavePress(payload);
|
||||||
}
|
}
|
||||||
@@ -129,7 +118,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
seedRatio,
|
seedRatio,
|
||||||
seedTime,
|
seedTime,
|
||||||
packSeedTime,
|
packSeedTime,
|
||||||
rejectBlocklistedTorrentHashesWhileGrabbing,
|
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
]);
|
]);
|
||||||
@@ -158,9 +146,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
case 'packSeedTime':
|
case 'packSeedTime':
|
||||||
setPackSeedTime(value);
|
setPackSeedTime(value);
|
||||||
break;
|
break;
|
||||||
case 'rejectBlocklistedTorrentHashesWhileGrabbing':
|
|
||||||
setRejectBlocklistedTorrentHashesWhileGrabbing(value);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||||
}
|
}
|
||||||
@@ -239,6 +224,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
name="seedRatio"
|
name="seedRatio"
|
||||||
value={seedRatio}
|
value={seedRatio}
|
||||||
helpText={translate('SeedRatioHelpText')}
|
helpText={translate('SeedRatioHelpText')}
|
||||||
|
isFloat={true}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -268,23 +254,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('IndexerSettingsRejectBlocklistedTorrentHashes')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="rejectBlocklistedTorrentHashesWhileGrabbing"
|
|
||||||
value={rejectBlocklistedTorrentHashesWhileGrabbing}
|
|
||||||
values={enableOptions}
|
|
||||||
helpText={translate(
|
|
||||||
'IndexerSettingsRejectBlocklistedTorrentHashesHelpText'
|
|
||||||
)}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
|||||||
@@ -47,9 +47,3 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointExtraSmall) {
|
|
||||||
.modalFooter {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -47,3 +47,42 @@ $hoverScale: 1.05;
|
|||||||
right: 0;
|
right: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloadLink {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
color: var(--textColor);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualDownloadContent {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
height: 20.39px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 20.39px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--iconButtonHoverColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactiveIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 0;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 8px;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ interface CssExports {
|
|||||||
'actions': string;
|
'actions': string;
|
||||||
'container': string;
|
'container': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
|
'downloadIcon': string;
|
||||||
|
'downloadLink': string;
|
||||||
'indexerRow': string;
|
'indexerRow': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'infoRow': string;
|
'infoRow': string;
|
||||||
|
'interactiveIcon': string;
|
||||||
|
'manualDownloadContent': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
'titleRow': string;
|
'titleRow': string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TextTruncate from 'react-text-truncate';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
|
||||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
|
||||||
import Peers from 'Search/Table/Peers';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './SearchIndexOverview.css';
|
|
||||||
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
|
|
||||||
function getContentHeight(rowHeight, isSmallScreen) {
|
|
||||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
|
||||||
|
|
||||||
return rowHeight - padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return icons.SPINNER;
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
} else if (grabError) {
|
|
||||||
return icons.DOWNLOADING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons.DOWNLOAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadKind(isGrabbed, grabError) {
|
|
||||||
if (isGrabbed) {
|
|
||||||
return kinds.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grabError) {
|
|
||||||
return kinds.DANGER;
|
|
||||||
}
|
|
||||||
|
|
||||||
return kinds.DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return '';
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return translate('AddedToDownloadClient');
|
|
||||||
} else if (grabError) {
|
|
||||||
return grabError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate('AddToDownloadClient');
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchIndexOverview extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onGrabPress({
|
|
||||||
guid,
|
|
||||||
indexerId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
infoUrl,
|
|
||||||
protocol,
|
|
||||||
downloadUrl,
|
|
||||||
magnetUrl,
|
|
||||||
categories,
|
|
||||||
seeders,
|
|
||||||
leechers,
|
|
||||||
indexerFlags,
|
|
||||||
size,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
indexer,
|
|
||||||
rowHeight,
|
|
||||||
isSmallScreen,
|
|
||||||
isGrabbed,
|
|
||||||
isGrabbing,
|
|
||||||
grabError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.info} style={{ height: contentHeight }}>
|
|
||||||
<div className={styles.titleRow}>
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Link
|
|
||||||
to={infoUrl}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<TextTruncate
|
|
||||||
line={2}
|
|
||||||
text={title}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={getDownloadKind(isGrabbed, grabError)}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadUrl || magnetUrl ?
|
|
||||||
<IconButton
|
|
||||||
name={icons.SAVE}
|
|
||||||
title={translate('Save')}
|
|
||||||
to={downloadUrl ?? magnetUrl}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.indexerRow}>
|
|
||||||
{indexer}
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoRow}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatAge(age, ageHours, ageMinutes)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<CategoryLabel
|
|
||||||
categories={categories}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
indexerFlags.length ?
|
|
||||||
indexerFlags
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
.map((flag, index) => {
|
|
||||||
return (
|
|
||||||
<Label key={index} kind={kinds.INFO}>
|
|
||||||
{titleCase(flag)}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}) :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchIndexOverview.propTypes = {
|
|
||||||
guid: PropTypes.string.isRequired,
|
|
||||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
age: PropTypes.number.isRequired,
|
|
||||||
ageHours: PropTypes.number.isRequired,
|
|
||||||
ageMinutes: PropTypes.number.isRequired,
|
|
||||||
publishDate: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
infoUrl: PropTypes.string.isRequired,
|
|
||||||
downloadUrl: PropTypes.string,
|
|
||||||
magnetUrl: PropTypes.string,
|
|
||||||
indexerId: PropTypes.number.isRequired,
|
|
||||||
indexer: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
files: PropTypes.number,
|
|
||||||
grabs: PropTypes.number,
|
|
||||||
seeders: PropTypes.number,
|
|
||||||
leechers: PropTypes.number,
|
|
||||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
rowHeight: PropTypes.number.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isGrabbed: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchIndexOverview.defaultProps = {
|
|
||||||
isGrabbing: false,
|
|
||||||
isGrabbed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchIndexOverview;
|
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||||
|
import { IndexerCategory } from 'Indexer/Indexer';
|
||||||
|
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
|
||||||
|
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||||
|
import Peers from 'Search/Table/Peers';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SearchIndexOverview.css';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
function getDownloadIcon(
|
||||||
|
isGrabbing: boolean,
|
||||||
|
isGrabbed: boolean,
|
||||||
|
grabError?: string
|
||||||
|
) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return icons.SPINNER;
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
} else if (grabError) {
|
||||||
|
return icons.DOWNLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons.DOWNLOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
|
||||||
|
if (isGrabbed) {
|
||||||
|
return kinds.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grabError) {
|
||||||
|
return kinds.DANGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinds.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadTooltip(
|
||||||
|
isGrabbing: boolean,
|
||||||
|
isGrabbed: boolean,
|
||||||
|
grabError?: string
|
||||||
|
) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return '';
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return translate('AddedToDownloadClient');
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('AddToDownloadClient');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchIndexOverviewProps {
|
||||||
|
guid: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
age: number;
|
||||||
|
ageHours: number;
|
||||||
|
ageMinutes: number;
|
||||||
|
publishDate: string;
|
||||||
|
title: string;
|
||||||
|
infoUrl: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
magnetUrl?: string;
|
||||||
|
indexerId: number;
|
||||||
|
indexer: string;
|
||||||
|
categories: IndexerCategory[];
|
||||||
|
size: number;
|
||||||
|
seeders?: number;
|
||||||
|
leechers?: number;
|
||||||
|
indexerFlags: string[];
|
||||||
|
isGrabbing: boolean;
|
||||||
|
isGrabbed: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
longDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
rowHeight: number;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
onGrabPress(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchIndexOverview(props: SearchIndexOverviewProps) {
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
protocol,
|
||||||
|
categories,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishDate,
|
||||||
|
title,
|
||||||
|
infoUrl,
|
||||||
|
downloadUrl,
|
||||||
|
magnetUrl,
|
||||||
|
indexer,
|
||||||
|
size,
|
||||||
|
seeders,
|
||||||
|
leechers,
|
||||||
|
indexerFlags = [],
|
||||||
|
isGrabbing = false,
|
||||||
|
isGrabbed = false,
|
||||||
|
grabError,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
onGrabPress,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { items: downloadClients } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGrabPressWrapper = useCallback(() => {
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
});
|
||||||
|
}, [guid, indexerId, onGrabPress]);
|
||||||
|
|
||||||
|
const onOverridePress = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(true);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const onOverrideModalClose = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(false);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const contentHeight = useMemo(() => {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}, [rowHeight, isSmallScreen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.info} style={{ height: contentHeight }}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Link to={infoUrl} title={title}>
|
||||||
|
<TextTruncate line={2} text={title} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={getDownloadKind(isGrabbed, grabError)}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPressWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<Link
|
||||||
|
className={styles.manualDownloadContent}
|
||||||
|
title={translate('OverrideAndAddToDownloadClient')}
|
||||||
|
onPress={onOverridePress}
|
||||||
|
>
|
||||||
|
<div className={styles.manualDownloadContent}>
|
||||||
|
<Icon
|
||||||
|
className={styles.interactiveIcon}
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
size={11}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
className={styles.downloadIcon}
|
||||||
|
name={icons.CIRCLE_DOWN}
|
||||||
|
size={9}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadUrl || magnetUrl ? (
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.SAVE}
|
||||||
|
title={translate('Save')}
|
||||||
|
to={downloadUrl ?? magnetUrl}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.indexerRow}>{indexer}</div>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
|
||||||
|
{protocol === 'torrent' && (
|
||||||
|
<Peers seeders={seeders} leechers={leechers} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Label>{formatBytes(size)}</Label>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<CategoryLabel categories={categories} />
|
||||||
|
|
||||||
|
{indexerFlags.length
|
||||||
|
? indexerFlags
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((flag, index) => {
|
||||||
|
return (
|
||||||
|
<Label key={index} kind={kinds.INFO}>
|
||||||
|
{titleCase(flag)}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OverrideMatchModal
|
||||||
|
isOpen={isOverrideModalOpen}
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onOverrideModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchIndexOverview;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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 (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||||
|
<SelectDownloadClientModalContent
|
||||||
|
protocol={protocol}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onDownloadClientSelect={onDownloadClientSelect}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectDownloadClientModal;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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 (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('SelectDownloadClientModalTitle', { modalTitle })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('DownloadClientsLoadError')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !error ? (
|
||||||
|
<Form>
|
||||||
|
{items.map((downloadClient) => {
|
||||||
|
const { id, name, priority } = downloadClient;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectDownloadClientRow
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
priority={priority}
|
||||||
|
onDownloadClientSelect={onDownloadClientSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectDownloadClientModalContent;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.downloadClient {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--borderColor);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'input': string;
|
'downloadClient': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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 (
|
||||||
|
<Link
|
||||||
|
className={styles.downloadClient}
|
||||||
|
component="div"
|
||||||
|
onPress={onSeasonSelectWrapper}
|
||||||
|
>
|
||||||
|
<div>{name}</div>
|
||||||
|
<div>{translate('PrioritySettings', { priority })}</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectDownloadClientRow;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 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;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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 (
|
||||||
|
<Link className={styles.link} isDisabled={isDisabled} onPress={onPress}>
|
||||||
|
{(value == null || (Array.isArray(value) && value.length === 0)) &&
|
||||||
|
!isDisabled ? (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
styles.placeholder,
|
||||||
|
isOptional && styles.optional
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideMatchData;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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 (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||||
|
<OverrideMatchModalContent
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideMatchModal;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 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;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
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<number | null>(null);
|
||||||
|
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||||
|
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 (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('OverrideGrabModalTitle', { title })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<DescriptionList>
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={
|
||||||
|
downloadClients.find(
|
||||||
|
(downloadClient) => downloadClient.id === downloadClientId
|
||||||
|
)?.name ?? translate('Default')
|
||||||
|
}
|
||||||
|
onPress={onSelectDownloadClientPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.footer}>
|
||||||
|
<div className={styles.error}>{grabError}</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
error={grabError}
|
||||||
|
onPress={onGrabPress}
|
||||||
|
>
|
||||||
|
{translate('GrabRelease')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<SelectDownloadClientModal
|
||||||
|
isOpen={selectModalOpen === 'downloadClient'}
|
||||||
|
protocol={protocol}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onDownloadClientSelect={onDownloadClientSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideMatchModalContent;
|
||||||
@@ -212,7 +212,11 @@ class SearchFooter extends Component {
|
|||||||
name="searchQuery"
|
name="searchQuery"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
buttons={
|
buttons={
|
||||||
<FormInputButton onPress={this.onQueryParameterModalOpenClick}>
|
<FormInputButton
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
onPress={this.onQueryParameterModalOpenClick}
|
||||||
|
title={translate('ClickToChangeQueryOptions')}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={icon}
|
name={icon}
|
||||||
/>
|
/>
|
||||||
@@ -275,6 +279,7 @@ class SearchFooter extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
className={styles.searchButton}
|
className={styles.searchButton}
|
||||||
isSpinning={isFetching}
|
isSpinning={isFetching}
|
||||||
isDisabled={isFetching || !hasIndexers}
|
isDisabled={isFetching || !hasIndexers}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
|
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 createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
|
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
|
||||||
import SearchIndex from './SearchIndex';
|
import SearchIndex from './SearchIndex';
|
||||||
@@ -55,12 +56,20 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
|
|
||||||
dispatchClearReleases() {
|
dispatchClearReleases() {
|
||||||
dispatch(clearReleases());
|
dispatch(clearReleases());
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchFetchDownloadClients() {
|
||||||
|
dispatch(fetchDownloadClients());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchIndexConnector extends Component {
|
class SearchIndexConnector extends Component {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchDownloadClients();
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.dispatchCancelFetchReleases();
|
this.props.dispatchCancelFetchReleases();
|
||||||
this.props.dispatchClearReleases();
|
this.props.dispatchClearReleases();
|
||||||
@@ -85,6 +94,7 @@ SearchIndexConnector.propTypes = {
|
|||||||
onBulkGrabPress: PropTypes.func.isRequired,
|
onBulkGrabPress: PropTypes.func.isRequired,
|
||||||
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||||
dispatchClearReleases: PropTypes.func.isRequired,
|
dispatchClearReleases: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchDownloadClients: PropTypes.func.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object)
|
items: PropTypes.arrayOf(PropTypes.object)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
|
|
||||||
function createReleaseSelector() {
|
function createReleaseSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@@ -37,10 +36,6 @@ function createMapStateToProps() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchExecuteCommand: executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class SearchIndexItemConnector extends Component {
|
class SearchIndexItemConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -71,4 +66,4 @@ SearchIndexItemConnector.propTypes = {
|
|||||||
component: PropTypes.elementType.isRequired
|
component: PropTypes.elementType.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
export default connect(createMapStateToProps, null)(SearchIndexItemConnector);
|
||||||
|
|||||||
@@ -67,3 +67,33 @@
|
|||||||
|
|
||||||
color: var(--textColor);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ interface CssExports {
|
|||||||
'category': string;
|
'category': string;
|
||||||
'cell': string;
|
'cell': string;
|
||||||
'checkInput': string;
|
'checkInput': string;
|
||||||
|
'downloadIcon': string;
|
||||||
'downloadLink': string;
|
'downloadLink': string;
|
||||||
'externalLinks': string;
|
'externalLinks': string;
|
||||||
'files': string;
|
'files': string;
|
||||||
'grabs': string;
|
'grabs': string;
|
||||||
'indexer': string;
|
'indexer': string;
|
||||||
'indexerFlags': string;
|
'indexerFlags': string;
|
||||||
|
'interactiveIcon': string;
|
||||||
|
'manualDownloadContent': string;
|
||||||
'peers': string;
|
'peers': string;
|
||||||
'protocol': string;
|
'protocol': string;
|
||||||
'size': string;
|
'size': string;
|
||||||
|
|||||||
@@ -1,431 +0,0 @@
|
|||||||
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 (
|
|
||||||
<VirtualTableSelectCell
|
|
||||||
inputClassName={styles.checkInput}
|
|
||||||
id={guid}
|
|
||||||
key={column.name}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isDisabled={false}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'protocol') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'age') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
|
|
||||||
>
|
|
||||||
{formatAge(age, ageHours, ageMinutes)}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'sortTitle') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={infoUrl}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{indexer}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'size') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'files') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{files}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'grabs') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{grabs}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'peers') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'category') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<CategoryLabel
|
|
||||||
categories={categories}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'indexerFlags') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!indexerFlags.length &&
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.FLAG}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('IndexerFlags')}
|
|
||||||
body={
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
indexerFlags.map((flag, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
{titleCase(flag)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.LEFT}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'actions') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={getDownloadKind(isGrabbed, grabError)}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadUrl ?
|
|
||||||
<IconButton
|
|
||||||
className={styles.downloadLink}
|
|
||||||
name={icons.SAVE}
|
|
||||||
title={translate('Save')}
|
|
||||||
onPress={this.onSavePress}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
magnetUrl ?
|
|
||||||
<IconButton
|
|
||||||
className={styles.downloadLink}
|
|
||||||
name={icons.MAGNET}
|
|
||||||
title={translate('Open')}
|
|
||||||
to={magnetUrl}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
imdbId || tmdbId || tvdbId || tvMazeId ? (
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
className={styles.externalLinks}
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
size={12}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<ReleaseLinks
|
|
||||||
categories={categories}
|
|
||||||
imdbId={imdbId}
|
|
||||||
tmdbId={tmdbId}
|
|
||||||
tvdbId={tvdbId}
|
|
||||||
tvMazeId={tvMazeId}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
kind={kinds.INVERSE}
|
|
||||||
position={tooltipPositions.TOP}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
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 (
|
||||||
|
<VirtualTableSelectCell
|
||||||
|
inputClassName={styles.checkInput}
|
||||||
|
id={guid}
|
||||||
|
key={name}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={false}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'protocol') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'age') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sortTitle') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<Link to={infoUrl} title={title}>
|
||||||
|
<div>{title}</div>
|
||||||
|
</Link>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{indexer}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{formatBytes(size)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'files') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{files}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'grabs') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{grabs}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'peers') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{protocol === 'torrent' && (
|
||||||
|
<Peers seeders={seeders} leechers={leechers} />
|
||||||
|
)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'category') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<CategoryLabel categories={categories} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexerFlags') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{!!indexerFlags.length && (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={
|
||||||
|
<ul>
|
||||||
|
{indexerFlags.map((flag, index) => {
|
||||||
|
return <li key={index}>{titleCase(flag)}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={getDownloadKind(isGrabbed, grabError)}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPressWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<Link
|
||||||
|
className={styles.manualDownloadContent}
|
||||||
|
title={translate('OverrideAndAddToDownloadClient')}
|
||||||
|
onPress={onOverridePress}
|
||||||
|
>
|
||||||
|
<div className={styles.manualDownloadContent}>
|
||||||
|
<Icon
|
||||||
|
className={styles.interactiveIcon}
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
className={styles.downloadIcon}
|
||||||
|
name={icons.CIRCLE_DOWN}
|
||||||
|
size={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadUrl ? (
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.SAVE}
|
||||||
|
title={translate('Save')}
|
||||||
|
onPress={onSavePressWrapper}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{magnetUrl ? (
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.MAGNET}
|
||||||
|
title={translate('Open')}
|
||||||
|
to={magnetUrl}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{imdbId || tmdbId || tvdbId || tvMazeId ? (
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.externalLinks}
|
||||||
|
name={icons.EXTERNAL_LINK}
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<ReleaseLinks
|
||||||
|
categories={categories}
|
||||||
|
imdbId={imdbId}
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
tvdbId={tvdbId}
|
||||||
|
tvMazeId={tvMazeId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<OverrideMatchModal
|
||||||
|
isOpen={isOverrideModalOpen}
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onOverrideModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchIndexRow;
|
||||||
+9
@@ -17,6 +17,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
|
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
|
||||||
import Category from './Categories/Category';
|
import Category from './Categories/Category';
|
||||||
@@ -61,6 +62,7 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteDownloadClientPress,
|
onDeleteDownloadClientPress,
|
||||||
onConfirmDeleteCategory,
|
onConfirmDeleteCategory,
|
||||||
...otherProps
|
...otherProps
|
||||||
@@ -219,6 +221,12 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
@@ -260,6 +268,7 @@ EditDownloadClientModalContent.propTypes = {
|
|||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteDownloadClientPress: PropTypes.func,
|
onDeleteDownloadClientPress: PropTypes.func,
|
||||||
onConfirmDeleteCategory: PropTypes.func.isRequired
|
onConfirmDeleteCategory: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
+17
-2
@@ -2,7 +2,15 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
deleteDownloadClientCategory,
|
||||||
|
fetchDownloadClientCategories,
|
||||||
|
saveDownloadClient,
|
||||||
|
setDownloadClientFieldValue,
|
||||||
|
setDownloadClientValue,
|
||||||
|
testDownloadClient,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||||
|
|
||||||
@@ -27,7 +35,8 @@ const mapDispatchToProps = {
|
|||||||
saveDownloadClient,
|
saveDownloadClient,
|
||||||
testDownloadClient,
|
testDownloadClient,
|
||||||
fetchDownloadClientCategories,
|
fetchDownloadClientCategories,
|
||||||
deleteDownloadClientCategory
|
deleteDownloadClientCategory,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditDownloadClientModalContentConnector extends Component {
|
class EditDownloadClientModalContentConnector extends Component {
|
||||||
@@ -68,6 +77,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
this.props.testDownloadClient({ id: this.props.id });
|
this.props.testDownloadClient({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
onConfirmDeleteCategory = (id) => {
|
onConfirmDeleteCategory = (id) => {
|
||||||
this.props.deleteDownloadClientCategory({ id });
|
this.props.deleteDownloadClientCategory({ id });
|
||||||
};
|
};
|
||||||
@@ -81,6 +94,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
|
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
|
||||||
@@ -102,6 +116,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
|||||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||||
saveDownloadClient: PropTypes.func.isRequired,
|
saveDownloadClient: PropTypes.func.isRequired,
|
||||||
testDownloadClient: PropTypes.func.isRequired,
|
testDownloadClient: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditIndexerProxyModalContent.css';
|
import styles from './EditIndexerProxyModalContent.css';
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ function EditIndexerProxyModalContent(props) {
|
|||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteIndexerProxyPress,
|
onDeleteIndexerProxyPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
@@ -130,6 +132,12 @@ function EditIndexerProxyModalContent(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
@@ -169,6 +177,7 @@ EditIndexerProxyModalContent.propTypes = {
|
|||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteIndexerProxyPress: PropTypes.func
|
onDeleteIndexerProxyPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveIndexerProxy, setIndexerProxyFieldValue, setIndexerProxyValue, testIndexerProxy } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveIndexerProxy,
|
||||||
|
setIndexerProxyFieldValue,
|
||||||
|
setIndexerProxyValue,
|
||||||
|
testIndexerProxy,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditIndexerProxyModalContent from './EditIndexerProxyModalContent';
|
import EditIndexerProxyModalContent from './EditIndexerProxyModalContent';
|
||||||
|
|
||||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||||||
setIndexerProxyValue,
|
setIndexerProxyValue,
|
||||||
setIndexerProxyFieldValue,
|
setIndexerProxyFieldValue,
|
||||||
saveIndexerProxy,
|
saveIndexerProxy,
|
||||||
testIndexerProxy
|
testIndexerProxy,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditIndexerProxyModalContentConnector extends Component {
|
class EditIndexerProxyModalContentConnector extends Component {
|
||||||
@@ -56,6 +63,10 @@ class EditIndexerProxyModalContentConnector extends Component {
|
|||||||
this.props.testIndexerProxy({ id: this.props.id });
|
this.props.testIndexerProxy({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -65,6 +76,7 @@ class EditIndexerProxyModalContentConnector extends Component {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
@@ -82,6 +94,7 @@ EditIndexerProxyModalContentConnector.propTypes = {
|
|||||||
setIndexerProxyFieldValue: PropTypes.func.isRequired,
|
setIndexerProxyFieldValue: PropTypes.func.isRequired,
|
||||||
saveIndexerProxy: PropTypes.func.isRequired,
|
saveIndexerProxy: PropTypes.func.isRequired,
|
||||||
testIndexerProxy: PropTypes.func.isRequired,
|
testIndexerProxy: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import NotificationEventItems from './NotificationEventItems';
|
import NotificationEventItems from './NotificationEventItems';
|
||||||
import styles from './EditNotificationModalContent.css';
|
import styles from './EditNotificationModalContent.css';
|
||||||
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
|||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteNotificationPress,
|
onDeleteNotificationPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
@@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
@@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
|
|||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteNotificationPress: PropTypes.func
|
onDeleteNotificationPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveNotification,
|
||||||
|
setNotificationFieldValue,
|
||||||
|
setNotificationValue,
|
||||||
|
testNotification,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||||
|
|
||||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||||||
setNotificationValue,
|
setNotificationValue,
|
||||||
setNotificationFieldValue,
|
setNotificationFieldValue,
|
||||||
saveNotification,
|
saveNotification,
|
||||||
testNotification
|
testNotification,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditNotificationModalContentConnector extends Component {
|
class EditNotificationModalContentConnector extends Component {
|
||||||
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
|||||||
this.props.testNotification({ id: this.props.id });
|
this.props.testNotification({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
|||||||
setNotificationFieldValue: PropTypes.func.isRequired,
|
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||||
saveNotification: PropTypes.func.isRequired,
|
saveNotification: PropTypes.func.isRequired,
|
||||||
testNotification: PropTypes.func.isRequired,
|
testNotification: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import $ from 'jquery';
|
||||||
|
import _ from 'lodash';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import getProviderState from 'Utilities/State/getProviderState';
|
import getProviderState from 'Utilities/State/getProviderState';
|
||||||
import { set } from '../baseActions';
|
import { set } from '../baseActions';
|
||||||
|
|
||||||
const abortCurrentRequests = {};
|
const abortCurrentRequests = {};
|
||||||
|
let lastTestData = null;
|
||||||
|
|
||||||
export function createCancelTestProviderHandler(section) {
|
export function createCancelTestProviderHandler(section) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
@@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
|||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
dispatch(set({ section, isTesting: true }));
|
dispatch(set({ section, isTesting: true }));
|
||||||
|
|
||||||
const testData = getProviderState(payload, getState, section);
|
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 ajaxOptions = {
|
const ajaxOptions = {
|
||||||
url: `${url}/test`,
|
url: `${url}/test?${$.param(params, true)}`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
@@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
|||||||
abortCurrentRequests[section] = abortRequest;
|
abortCurrentRequests[section] = abortRequest;
|
||||||
|
|
||||||
request.done((data) => {
|
request.done((data) => {
|
||||||
|
lastTestData = null;
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section,
|
||||||
isTesting: false,
|
isTesting: false,
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ export const defaultState = {
|
|||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'host',
|
||||||
|
label: () => translate('Host'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'elapsedTime',
|
name: 'elapsedTime',
|
||||||
label: () => translate('ElapsedTime'),
|
label: () => translate('ElapsedTime'),
|
||||||
|
|||||||
@@ -401,7 +401,16 @@ export const actionHandlers = handleThunks({
|
|||||||
export const reducers = createHandleActions({
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
[CLEAR_RELEASES]: (state) => {
|
[CLEAR_RELEASES]: (state) => {
|
||||||
return Object.assign({}, state, defaultState);
|
const {
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
customFilters,
|
||||||
|
selectedFilterKey,
|
||||||
|
columns,
|
||||||
|
...otherDefaultState
|
||||||
|
} = defaultState;
|
||||||
|
|
||||||
|
return Object.assign({}, state, otherDefaultState);
|
||||||
},
|
},
|
||||||
|
|
||||||
[UPDATE_RELEASE]: (state, { payload }) => {
|
[UPDATE_RELEASE]: (state, { payload }) => {
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
function createDimensionsSelector() {
|
function createDimensionsSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.app.dimensions,
|
(state: AppState) => state.app.dimensions,
|
||||||
(dimensions) => {
|
(dimensions) => {
|
||||||
return dimensions;
|
return dimensions;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
|
||||||
|
export default function createEnabledDownloadClientsSelector(
|
||||||
|
protocol: DownloadProtocol
|
||||||
|
) {
|
||||||
|
return createSelector(
|
||||||
|
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||||
|
(downloadClients: DownloadClientAppState) => {
|
||||||
|
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||||
|
|
||||||
|
const clients = items.filter(
|
||||||
|
(item) => item.protocol === protocol && item.enable
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isFetching, isPopulated, error, items: clients };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -188,7 +188,7 @@ module.exports = {
|
|||||||
// Charts
|
// Charts
|
||||||
|
|
||||||
chartBackgroundColor: '#262626',
|
chartBackgroundColor: '#262626',
|
||||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
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'],
|
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']
|
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as dark from './dark';
|
|||||||
import * as light from './light';
|
import * as light from './light';
|
||||||
|
|
||||||
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
const auto = defaultDark ? { ...dark } : { ...light };
|
const auto = defaultDark ? dark : light;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
auto,
|
auto,
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ module.exports = {
|
|||||||
// Charts
|
// Charts
|
||||||
|
|
||||||
chartBackgroundColor: '#fff',
|
chartBackgroundColor: '#fff',
|
||||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
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'],
|
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']
|
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ module.exports = {
|
|||||||
// Families
|
// Families
|
||||||
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
|
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
|
||||||
passwordFamily: 'text-security-disc',
|
|
||||||
|
|
||||||
// Sizes
|
// Sizes
|
||||||
extraSmallFontSize: '11px',
|
extraSmallFontSize: '11px',
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ function getInternalLink(source) {
|
|||||||
to="/settings/connect"
|
to="/settings/connect"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'IndexerProxyStatusCheck':
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
name={icons.SETTINGS}
|
||||||
|
title={translate('Settings')}
|
||||||
|
to="/settings/indexers"
|
||||||
|
/>
|
||||||
|
);
|
||||||
case 'IndexerRssCheck':
|
case 'IndexerRssCheck':
|
||||||
case 'IndexerSearchCheck':
|
case 'IndexerSearchCheck':
|
||||||
case 'IndexerStatusCheck':
|
case 'IndexerStatusCheck':
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ function formatBytes(input) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesize(size, {
|
return `${filesize(size, {
|
||||||
base: 2,
|
base: 2,
|
||||||
round: 1
|
round: 1
|
||||||
});
|
})}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default formatBytes;
|
export default formatBytes;
|
||||||
|
|||||||
+76
-20
@@ -14,7 +14,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|
||||||
<meta name="description" content="Prowlarr (Preview)" />
|
<meta name="description" content="Prowlarr" />
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #f5f7fa;
|
background-color: var(--pageBackground);
|
||||||
color: #656565;
|
color: var(--textColor);
|
||||||
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
@@ -88,14 +88,14 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
background-color: #464b51;
|
background-color: var(--themeDarkColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
background-color: #fff;
|
background-color: var(--panelBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sign-in {
|
.sign-in {
|
||||||
@@ -112,16 +112,18 @@
|
|||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
border: 1px solid #dde6e9;
|
background-color: var(--inputBackgroundColor);
|
||||||
|
border: 1px solid var(--inputBorderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
|
||||||
|
color: var(--textColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input:focus {
|
.form-input:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-color: #66afe9;
|
border-color: var(--inputFocusBorderColor);
|
||||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
|
||||||
0 0 8px rgba(102, 175, 233, 0.6);
|
0 0 8px var(--inputFocusBoxShadowColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@@ -130,10 +132,10 @@
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-color: #5899eb;
|
border-color: var(--primaryBorderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #5d9cec;
|
background-color: var(--primaryBackgroundColor);
|
||||||
color: #fff;
|
color: var(--white);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -141,9 +143,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
border-color: #3483e7;
|
border-color: var(--primaryHoverBorderColor);
|
||||||
background-color: #4b91ea;
|
background-color: var(--primaryHoverBackgroundColor);
|
||||||
color: #fff;
|
color: var(--white);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,24 +167,24 @@
|
|||||||
|
|
||||||
.forgot-password {
|
.forgot-password {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: #909fa7;
|
color: var(--forgotPasswordColor);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password:focus,
|
.forgot-password:focus,
|
||||||
.forgot-password:hover {
|
.forgot-password:hover {
|
||||||
color: #748690;
|
color: var(--forgotPasswordAltColor);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password:visited {
|
.forgot-password:visited {
|
||||||
color: #748690;
|
color: var(--forgotPasswordAltColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-failed {
|
.login-failed {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: #f05050;
|
color: var(--failedColor);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,5 +293,59 @@
|
|||||||
|
|
||||||
loginFailedDiv.classList.remove("hidden");
|
loginFailedDiv.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var light = {
|
||||||
|
white: '#fff',
|
||||||
|
pageBackground: '#f5f7fa',
|
||||||
|
textColor: '#515253',
|
||||||
|
themeDarkColor: '#464b51',
|
||||||
|
panelBackground: '#fff',
|
||||||
|
inputBackgroundColor: '#fff',
|
||||||
|
inputBorderColor: '#dde6e9',
|
||||||
|
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||||
|
inputFocusBorderColor: '#66afe9',
|
||||||
|
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||||
|
primaryBackgroundColor: '#5d9cec',
|
||||||
|
primaryBorderColor: '#5899eb',
|
||||||
|
primaryHoverBackgroundColor: '#4b91ea',
|
||||||
|
primaryHoverBorderColor: '#3483e7',
|
||||||
|
failedColor: '#f05050',
|
||||||
|
forgotPasswordColor: '#909fa7',
|
||||||
|
forgotPasswordAltColor: '#748690'
|
||||||
|
};
|
||||||
|
|
||||||
|
var dark = {
|
||||||
|
white: '#fff',
|
||||||
|
pageBackground: '#202020',
|
||||||
|
textColor: '#ccc',
|
||||||
|
themeDarkColor: '#494949',
|
||||||
|
panelBackground: '#111',
|
||||||
|
inputBackgroundColor: '#333',
|
||||||
|
inputBorderColor: '#dde6e9',
|
||||||
|
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||||
|
inputFocusBorderColor: '#66afe9',
|
||||||
|
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||||
|
primaryBackgroundColor: '#5d9cec',
|
||||||
|
primaryBorderColor: '#5899eb',
|
||||||
|
primaryHoverBackgroundColor: '#4b91ea',
|
||||||
|
primaryHoverBorderColor: '#3483e7',
|
||||||
|
failedColor: '#f05050',
|
||||||
|
forgotPasswordColor: '#737d83',
|
||||||
|
forgotPasswordAltColor: '#546067'
|
||||||
|
};
|
||||||
|
|
||||||
|
var theme = "_THEME_";
|
||||||
|
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
|
||||||
|
dark :
|
||||||
|
light;
|
||||||
|
|
||||||
|
Object.entries(finalTheme).forEach(([key, value]) => {
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
`--${key}`,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
|
theme: 'auto' | 'dark' | 'light';
|
||||||
showRelativeDates: boolean;
|
showRelativeDates: boolean;
|
||||||
shortDateFormat: string;
|
shortDateFormat: string;
|
||||||
longDateFormat: string;
|
longDateFormat: string;
|
||||||
|
|||||||
+22
-22
@@ -30,12 +30,12 @@
|
|||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@juggle/resize-observer": "3.4.0",
|
"@juggle/resize-observer": "3.4.0",
|
||||||
"@microsoft/signalr": "6.0.25",
|
"@microsoft/signalr": "6.0.25",
|
||||||
"@sentry/browser": "7.51.2",
|
"@sentry/browser": "7.100.0",
|
||||||
"@sentry/integrations": "7.51.2",
|
"@sentry/integrations": "7.100.0",
|
||||||
"@types/node": "18.15.11",
|
"@types/node": "18.19.31",
|
||||||
"@types/react": "18.2.6",
|
"@types/react": "18.2.79",
|
||||||
"@types/react-dom": "18.2.4",
|
"@types/react-dom": "18.2.25",
|
||||||
"chart.js": "4.3.0",
|
"chart.js": "4.4.2",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"connected-react-router": "6.9.3",
|
"connected-react-router": "6.9.3",
|
||||||
@@ -83,46 +83,46 @@
|
|||||||
"redux-thunk": "2.4.2",
|
"redux-thunk": "2.4.2",
|
||||||
"reselect": "4.1.7",
|
"reselect": "4.1.7",
|
||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.22.11",
|
"@babel/core": "7.24.4",
|
||||||
"@babel/eslint-parser": "7.22.11",
|
"@babel/eslint-parser": "7.24.1",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.22.5",
|
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.22.14",
|
"@babel/preset-env": "7.24.4",
|
||||||
"@babel/preset-react": "7.22.5",
|
"@babel/preset-react": "7.24.1",
|
||||||
"@babel/preset-typescript": "7.22.11",
|
"@babel/preset-typescript": "7.24.1",
|
||||||
"@types/lodash": "4.14.194",
|
"@types/lodash": "4.14.194",
|
||||||
"@types/react-router-dom": "5.3.3",
|
"@types/react-router-dom": "5.3.3",
|
||||||
"@types/react-text-truncate": "0.14.1",
|
"@types/react-text-truncate": "0.14.1",
|
||||||
"@types/react-window": "1.8.5",
|
"@types/react-window": "1.8.5",
|
||||||
"@types/webpack-livereload-plugin": "2.3.3",
|
"@types/webpack-livereload-plugin": "2.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "5.59.5",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"are-you-es5": "2.1.2",
|
"are-you-es5": "2.1.2",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.33.0",
|
"core-js": "3.37.0",
|
||||||
"css-loader": "6.7.3",
|
"css-loader": "6.7.3",
|
||||||
"css-modules-typescript-loader": "4.0.1",
|
"css-modules-typescript-loader": "4.0.1",
|
||||||
"eslint": "8.45.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.10.0",
|
||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.34.1",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"filemanager-webpack-plugin": "8.0.0",
|
"filemanager-webpack-plugin": "8.0.0",
|
||||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||||
"html-webpack-plugin": "5.5.1",
|
"html-webpack-plugin": "5.5.1",
|
||||||
"loader-utils": "^3.2.1",
|
"loader-utils": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.7.5",
|
"mini-css-extract-plugin": "2.7.5",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.38",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
"postcss-loader": "7.3.0",
|
"postcss-loader": "7.3.0",
|
||||||
"postcss-mixins": "9.0.4",
|
"postcss-mixins": "9.0.4",
|
||||||
|
|||||||
@@ -99,20 +99,52 @@
|
|||||||
<RootNamespace Condition="'$(ProwlarrProject)'=='true'">$(MSBuildProjectName.Replace('Prowlarr','NzbDrone'))</RootNamespace>
|
<RootNamespace Condition="'$(ProwlarrProject)'=='true'">$(MSBuildProjectName.Replace('Prowlarr','NzbDrone'))</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
<ItemGroup Condition="'$(TestProject)'!='true'">
|
||||||
<!-- FXCop Built into Net5 SDK now as NETAnalyzers, Enabled by default on net5 projects -->
|
<!-- Annotates .NET assemblies with repository information including SHA -->
|
||||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
||||||
|
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
||||||
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Sentry specific configuration: Only in Release mode -->
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
||||||
|
<!-- OrgSlug, ProjectSlug and AuthToken are required.
|
||||||
|
They can be set below, via argument to 'msbuild -p:' or environment variable -->
|
||||||
|
<SentryOrg></SentryOrg>
|
||||||
|
<SentryProject></SentryProject>
|
||||||
|
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
|
||||||
|
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
|
||||||
|
|
||||||
|
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
|
||||||
|
without the need to deploy the application with PDBs -->
|
||||||
|
<SentryUploadSymbols>true</SentryUploadSymbols>
|
||||||
|
|
||||||
|
<!-- Source Link settings -->
|
||||||
|
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
|
||||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
|
||||||
|
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
|
||||||
|
<EmbedAllSources>true</EmbedAllSources>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
|
||||||
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
||||||
|
<!-- FXCop Built into Net5 SDK now as NETAnalyzers, Enabled by default on net5 projects -->
|
||||||
|
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Set up stylecop -->
|
<!-- Set up stylecop -->
|
||||||
<ItemGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'!='false'">
|
<ItemGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'!='false'">
|
||||||
<!-- StyleCop analysis -->
|
<!-- StyleCop analysis -->
|
||||||
@@ -144,16 +176,46 @@
|
|||||||
</Otherwise>
|
</Otherwise>
|
||||||
</Choose>
|
</Choose>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Set architecture to RuntimeInformation.ProcessArchitecture if not specified -->
|
||||||
|
<Choose>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>x64</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>x86</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>arm64</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>arm</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<Otherwise>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture></Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Otherwise>
|
||||||
|
</Choose>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
||||||
'$(RuntimeIdentifier)' == ''">
|
'$(RuntimeIdentifier)' == ''">
|
||||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
||||||
'$(RuntimeIdentifier)' == ''">
|
'$(RuntimeIdentifier)' == ''">
|
||||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Options;
|
||||||
using NzbDrone.Core.Authentication;
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
@@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test
|
|||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
|
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
|
||||||
.Callback<string, string>((p, t) => _configFileContents = t);
|
.Callback<string, string>((p, t) => _configFileContents = t);
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<AuthOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new AuthOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<AppOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new AppOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<ServerOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new ServerOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<LogOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new LogOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<UpdateOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new UpdateOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Instrumentation.Sentry;
|
using NzbDrone.Common.Instrumentation.Sentry;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
|
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
@@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test
|
|||||||
[TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")]
|
[TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")]
|
||||||
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
|
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
|
||||||
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
|
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
|
||||||
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
|
[TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")]
|
||||||
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
|
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
|
||||||
public void Clean_Path_Windows(string dirty, string clean)
|
public void Clean_Path_Windows(string dirty, string clean)
|
||||||
{
|
{
|
||||||
@@ -315,5 +316,30 @@ namespace NzbDrone.Common.Test
|
|||||||
result[2].Should().Be(@"TV");
|
result[2].Should().Be(@"TV");
|
||||||
result[3].Should().Be(@"Series Title");
|
result[3].Should().Be(@"Series Title");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(@"C:\Test\")]
|
||||||
|
[TestCase(@"C:\Test")]
|
||||||
|
[TestCase(@"C:\Test\TV\")]
|
||||||
|
[TestCase(@"C:\Test\TV")]
|
||||||
|
public void IsPathValid_should_be_true(string path)
|
||||||
|
{
|
||||||
|
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(@"C:\Test \")]
|
||||||
|
[TestCase(@"C:\Test ")]
|
||||||
|
[TestCase(@"C:\ Test\")]
|
||||||
|
[TestCase(@"C:\ Test")]
|
||||||
|
[TestCase(@"C:\Test \TV")]
|
||||||
|
[TestCase(@"C:\ Test\TV")]
|
||||||
|
[TestCase(@"C:\Test \TV\")]
|
||||||
|
[TestCase(@"C:\ Test\TV\")]
|
||||||
|
[TestCase(@" C:\Test\TV\")]
|
||||||
|
[TestCase(@" C:\Test\TV")]
|
||||||
|
|
||||||
|
public void IsPathValid_should_be_false(string path)
|
||||||
|
{
|
||||||
|
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using NUnit.Framework;
|
|||||||
using NzbDrone.Common.Composition.Extensions;
|
using NzbDrone.Common.Composition.Extensions;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Common.Options;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Datastore.Extensions;
|
using NzbDrone.Core.Datastore.Extensions;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
@@ -29,10 +30,16 @@ namespace NzbDrone.Common.Test
|
|||||||
.AddNzbDroneLogger()
|
.AddNzbDroneLogger()
|
||||||
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
||||||
.AddDummyDatabase()
|
.AddDummyDatabase()
|
||||||
|
.AddDummyLogDatabase()
|
||||||
.AddStartupContext(new StartupContext("first", "second"));
|
.AddStartupContext(new StartupContext("first", "second"));
|
||||||
|
|
||||||
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
||||||
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object);
|
||||||
|
|
||||||
var serviceProvider = container.GetServiceProvider();
|
var serviceProvider = container.GetServiceProvider();
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,11 @@ namespace NzbDrone.Common.Disk
|
|||||||
{
|
{
|
||||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||||
|
|
||||||
return Directory.EnumerateDirectories(path);
|
return Directory.EnumerateDirectories(path, "*", new EnumerationOptions
|
||||||
|
{
|
||||||
|
AttributesToSkip = FileAttributes.System,
|
||||||
|
IgnoreInaccessible = true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> GetFiles(string path, bool recursive)
|
public IEnumerable<string> GetFiles(string path, bool recursive)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
@@ -28,6 +29,12 @@ namespace NzbDrone.Common.Extensions
|
|||||||
|
|
||||||
public static string CleanFilePath(this string path)
|
public static string CleanFilePath(this string path)
|
||||||
{
|
{
|
||||||
|
if (path.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
// Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix.
|
||||||
|
path = path.TrimEnd(' ');
|
||||||
|
}
|
||||||
|
|
||||||
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
||||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
|
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
|
||||||
|
|
||||||
@@ -36,10 +43,10 @@ namespace NzbDrone.Common.Extensions
|
|||||||
// UNC
|
// UNC
|
||||||
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
|
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
|
||||||
{
|
{
|
||||||
return info.FullName.TrimEnd('/', '\\', ' ');
|
return info.FullName.TrimEnd('/', '\\');
|
||||||
}
|
}
|
||||||
|
|
||||||
return info.FullName.TrimEnd('/').Trim('\\', ' ');
|
return info.FullName.TrimEnd('/').Trim('\\');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
|
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
|
||||||
@@ -143,6 +150,23 @@ namespace NzbDrone.Common.Extensions
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path.Trim() != path)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
|
|
||||||
|
while (directoryInfo != null)
|
||||||
|
{
|
||||||
|
if (directoryInfo.Name.Trim() != directoryInfo.Name)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryInfo = directoryInfo.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
if (validationType == PathValidationType.AnyOs)
|
if (validationType == PathValidationType.AnyOs)
|
||||||
{
|
{
|
||||||
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
|
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
|
||||||
@@ -253,6 +277,11 @@ namespace NzbDrone.Common.Extensions
|
|||||||
return processName;
|
return processName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string CleanPath(this string path)
|
||||||
|
{
|
||||||
|
return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
return appFolderInfo.AppDataFolder;
|
return appFolderInfo.AppDataFolder;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.Net.Sockets;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
@@ -31,11 +32,14 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
||||||
private readonly ICached<CredentialCache> _credentialCache;
|
private readonly ICached<CredentialCache> _credentialCache;
|
||||||
|
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
||||||
ICreateManagedWebProxy createManagedWebProxy,
|
ICreateManagedWebProxy createManagedWebProxy,
|
||||||
ICertificateValidationService certificateValidationService,
|
ICertificateValidationService certificateValidationService,
|
||||||
IUserAgentBuilder userAgentBuilder,
|
IUserAgentBuilder userAgentBuilder,
|
||||||
ICacheManager cacheManager)
|
ICacheManager cacheManager,
|
||||||
|
Logger logger)
|
||||||
{
|
{
|
||||||
_proxySettingsProvider = proxySettingsProvider;
|
_proxySettingsProvider = proxySettingsProvider;
|
||||||
_createManagedWebProxy = createManagedWebProxy;
|
_createManagedWebProxy = createManagedWebProxy;
|
||||||
@@ -44,6 +48,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
|
|
||||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
||||||
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||||
@@ -277,19 +283,27 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasRoutableIPv4Address()
|
private bool HasRoutableIPv4Address()
|
||||||
{
|
{
|
||||||
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
try
|
||||||
|
{
|
||||||
|
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||||
|
|
||||||
return networkInterfaces.Any(ni =>
|
return networkInterfaces.Any(ni =>
|
||||||
ni.OperationalStatus == OperationalStatus.Up &&
|
ni.OperationalStatus == OperationalStatus.Up &&
|
||||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||||
!IPAddress.IsLoopback(ip.Address)));
|
!IPAddress.IsLoopback(ip.Address)));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
||||||
@@ -313,7 +327,9 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||||
useIPv6 = !HasRoutableIPv4Address();
|
var routableIPv4 = HasRoutableIPv4Address();
|
||||||
|
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
|
||||||
|
useIPv6 = !routableIPv4;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
RegisterDebugger();
|
RegisterDebugger();
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterSentry(updateApp);
|
RegisterSentry(updateApp, appFolderInfo);
|
||||||
|
|
||||||
if (updateApp)
|
if (updateApp)
|
||||||
{
|
{
|
||||||
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
LogManager.ReconfigExistingLoggers();
|
LogManager.ReconfigExistingLoggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterSentry(bool updateClient)
|
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
string dsn;
|
string dsn;
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
: "https://e38306161ff945999adf774a16e933c3@sentry.servarr.com/30";
|
: "https://e38306161ff945999adf774a16e933c3@sentry.servarr.com/30";
|
||||||
}
|
}
|
||||||
|
|
||||||
var target = new SentryTarget(dsn)
|
var target = new SentryTarget(dsn, appFolderInfo)
|
||||||
{
|
{
|
||||||
Name = "sentryTarget",
|
Name = "sentryTarget",
|
||||||
Layout = "${message}"
|
Layout = "${message}"
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
public bool FilterEvents { get; set; }
|
public bool FilterEvents { get; set; }
|
||||||
public bool SentryEnabled { get; set; }
|
public bool SentryEnabled { get; set; }
|
||||||
|
|
||||||
public SentryTarget(string dsn)
|
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
_sdk = SentrySdk.Init(o =>
|
_sdk = SentrySdk.Init(o =>
|
||||||
{
|
{
|
||||||
@@ -114,9 +114,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.AttachStacktrace = true;
|
o.AttachStacktrace = true;
|
||||||
o.MaxBreadcrumbs = 200;
|
o.MaxBreadcrumbs = 200;
|
||||||
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
||||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
|
||||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
|
||||||
o.Environment = BuildInfo.Branch;
|
o.Environment = BuildInfo.Branch;
|
||||||
|
|
||||||
|
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
||||||
|
o.AutoSessionTracking = true;
|
||||||
|
|
||||||
|
// Caches files in the event device is offline
|
||||||
|
// Sentry creates a 'sentry' sub directory, no need to concat here
|
||||||
|
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
|
||||||
|
|
||||||
|
// default environment is production
|
||||||
|
if (!RuntimeInfo.IsProduction)
|
||||||
|
{
|
||||||
|
if (RuntimeInfo.IsDevelopment)
|
||||||
|
{
|
||||||
|
o.Environment = "development";
|
||||||
|
}
|
||||||
|
else if (RuntimeInfo.IsTesting)
|
||||||
|
{
|
||||||
|
o.Environment = "testing";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
o.Environment = "other";
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
InitializeScope();
|
InitializeScope();
|
||||||
@@ -134,7 +158,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
{
|
{
|
||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
scope.User = new User
|
scope.User = new SentryUser
|
||||||
{
|
{
|
||||||
Id = HashUtil.AnonymousToken()
|
Id = HashUtil.AnonymousToken()
|
||||||
};
|
};
|
||||||
@@ -317,13 +341,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var level = LoggingLevelMap[logEvent.Level];
|
||||||
var sentryEvent = new SentryEvent(logEvent.Exception)
|
var sentryEvent = new SentryEvent(logEvent.Exception)
|
||||||
{
|
{
|
||||||
Level = LoggingLevelMap[logEvent.Level],
|
Level = level,
|
||||||
Logger = logEvent.LoggerName,
|
Logger = logEvent.LoggerName,
|
||||||
Message = logEvent.FormattedMessage
|
Message = logEvent.FormattedMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
|
||||||
|
{
|
||||||
|
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
|
||||||
|
// the 'unhandled' exception flag
|
||||||
|
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
|
||||||
|
}
|
||||||
|
|
||||||
sentryEvent.SetExtras(extras);
|
sentryEvent.SetExtras(extras);
|
||||||
sentryEvent.SetFingerprint(fingerPrint);
|
sentryEvent.SetFingerprint(fingerPrint);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class AppOptions
|
||||||
|
{
|
||||||
|
public string InstanceName { get; set; }
|
||||||
|
public string Theme { get; set; }
|
||||||
|
public bool? LaunchBrowser { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class AuthOptions
|
||||||
|
{
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
public bool? Enabled { get; set; }
|
||||||
|
public string Method { get; set; }
|
||||||
|
public string Required { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class LogOptions
|
||||||
|
{
|
||||||
|
public string Level { get; set; }
|
||||||
|
public bool? FilterSentryEvents { get; set; }
|
||||||
|
public int? Rotate { get; set; }
|
||||||
|
public bool? Sql { get; set; }
|
||||||
|
public string ConsoleLevel { get; set; }
|
||||||
|
public bool? AnalyticsEnabled { get; set; }
|
||||||
|
public string SyslogServer { get; set; }
|
||||||
|
public int? SyslogPort { get; set; }
|
||||||
|
public string SyslogLevel { get; set; }
|
||||||
|
public bool? DbEnabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class ServerOptions
|
||||||
|
{
|
||||||
|
public string UrlBase { get; set; }
|
||||||
|
public string BindAddress { get; set; }
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public bool? EnableSsl { get; set; }
|
||||||
|
public int? SslPort { get; set; }
|
||||||
|
public string SslCertPath { get; set; }
|
||||||
|
public string SslCertPassword { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class UpdateOptions
|
||||||
|
{
|
||||||
|
public string Mechanism { get; set; }
|
||||||
|
public bool? Automatically { get; set; }
|
||||||
|
public string ScriptPath { get; set; }
|
||||||
|
public string Branch { get; set; }
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="5.2.0" />
|
<PackageReference Include="NLog" Version="5.2.0" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
|
||||||
<PackageReference Include="Npgsql" Version="7.0.6" />
|
<PackageReference Include="Npgsql" Version="7.0.7" />
|
||||||
<PackageReference Include="Sentry" Version="3.29.1" />
|
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
|
|||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class newznab_category_to_capabilities_settingsFixture : MigrationTest<newznab_category_to_capabilities_settings>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_migrate_categories_when_capabilities_is_not_defined()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Indexers").Row(new
|
||||||
|
{
|
||||||
|
Name = "Usenet Indexer",
|
||||||
|
Redirect = false,
|
||||||
|
AppProfileId = 0,
|
||||||
|
DownloadClientId = 0,
|
||||||
|
Priority = 25,
|
||||||
|
Added = DateTime.UtcNow,
|
||||||
|
Implementation = "Newznab",
|
||||||
|
Settings = new
|
||||||
|
{
|
||||||
|
Categories = new[]
|
||||||
|
{
|
||||||
|
new { Id = 2000, Name = "Movies" },
|
||||||
|
new { Id = 5000, Name = "TV" }
|
||||||
|
}
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "NewznabSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Implementation.Should().Be("Newznab");
|
||||||
|
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||||
|
items.First().Settings.Should().ContainKey("capabilities");
|
||||||
|
items.First().Settings.Should().NotContainKey("categories");
|
||||||
|
|
||||||
|
var newznabSettings = items.First().Settings.ToObject<NewznabSettings40>();
|
||||||
|
newznabSettings.Capabilities.Should().NotBeNull();
|
||||||
|
newznabSettings.Capabilities.SupportsRawSearch.Should().Be(false);
|
||||||
|
newznabSettings.Capabilities.Categories.Should().HaveCount(2);
|
||||||
|
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 2000 && c.Name == "Movies");
|
||||||
|
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 5000 && c.Name == "TV");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_migrate_categories_when_capabilities_is_defined()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Indexers").Row(new
|
||||||
|
{
|
||||||
|
Name = "Usenet Indexer",
|
||||||
|
Redirect = false,
|
||||||
|
AppProfileId = 0,
|
||||||
|
DownloadClientId = 0,
|
||||||
|
Priority = 25,
|
||||||
|
Added = DateTime.UtcNow,
|
||||||
|
Implementation = "Newznab",
|
||||||
|
Settings = new
|
||||||
|
{
|
||||||
|
Capabilities = new
|
||||||
|
{
|
||||||
|
SupportsRawSearch = true
|
||||||
|
},
|
||||||
|
Categories = new[]
|
||||||
|
{
|
||||||
|
new { Id = 2000, Name = "Movies" },
|
||||||
|
new { Id = 5000, Name = "TV" }
|
||||||
|
}
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "NewznabSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Implementation.Should().Be("Newznab");
|
||||||
|
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||||
|
items.First().Settings.Should().ContainKey("capabilities");
|
||||||
|
items.First().Settings.Should().NotContainKey("categories");
|
||||||
|
|
||||||
|
var newznabSettings = items.First().Settings.ToObject<NewznabSettings40>();
|
||||||
|
newznabSettings.Capabilities.Should().NotBeNull();
|
||||||
|
newznabSettings.Capabilities.SupportsRawSearch.Should().Be(true);
|
||||||
|
newznabSettings.Capabilities.Categories.Should().HaveCount(2);
|
||||||
|
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 2000 && c.Name == "Movies");
|
||||||
|
newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 5000 && c.Name == "TV");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_defaults_when_categories_are_empty()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Indexers").Row(new
|
||||||
|
{
|
||||||
|
Name = "Usenet Indexer",
|
||||||
|
Redirect = false,
|
||||||
|
AppProfileId = 0,
|
||||||
|
DownloadClientId = 0,
|
||||||
|
Priority = 25,
|
||||||
|
Added = DateTime.UtcNow,
|
||||||
|
Implementation = "Newznab",
|
||||||
|
Settings = new
|
||||||
|
{
|
||||||
|
Categories = Array.Empty<object>()
|
||||||
|
}.ToJson(),
|
||||||
|
ConfigContract = "NewznabSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Implementation.Should().Be("Newznab");
|
||||||
|
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||||
|
items.First().Settings.Should().ContainKey("capabilities");
|
||||||
|
items.First().Settings.Should().NotContainKey("categories");
|
||||||
|
|
||||||
|
var newznabSettings = items.First().Settings.ToObject<NewznabSettings40>();
|
||||||
|
newznabSettings.Capabilities.Should().NotBeNull();
|
||||||
|
newznabSettings.Capabilities.SupportsRawSearch.Should().Be(false);
|
||||||
|
newznabSettings.Capabilities.Categories.Should().NotBeNull();
|
||||||
|
newznabSettings.Capabilities.Categories.Should().HaveCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_defaults_when_settings_are_empty()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("Indexers").Row(new
|
||||||
|
{
|
||||||
|
Name = "Usenet Indexer",
|
||||||
|
Redirect = false,
|
||||||
|
AppProfileId = 0,
|
||||||
|
DownloadClientId = 0,
|
||||||
|
Priority = 25,
|
||||||
|
Added = DateTime.UtcNow,
|
||||||
|
Implementation = "Newznab",
|
||||||
|
Settings = new { }.ToJson(),
|
||||||
|
ConfigContract = "NewznabSettings"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<IndexerDefinition40>("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Implementation.Should().Be("Newznab");
|
||||||
|
items.First().ConfigContract.Should().Be("NewznabSettings");
|
||||||
|
items.First().Settings.Should().NotContainKey("capabilities");
|
||||||
|
items.First().Settings.Should().NotContainKey("categories");
|
||||||
|
items.First().Settings.ToObject<NewznabSettings40>().Capabilities.Should().BeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexerDefinition40
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Implementation { get; set; }
|
||||||
|
public string ConfigContract { get; set; }
|
||||||
|
public JObject Settings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NewznabSettings39
|
||||||
|
{
|
||||||
|
public object Categories { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NewznabSettings40
|
||||||
|
{
|
||||||
|
public NewznabCapabilitiesSettings40 Capabilities { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NewznabCapabilitiesSettings40
|
||||||
|
{
|
||||||
|
public bool SupportsRawSearch { get; set; }
|
||||||
|
public List<IndexerCategory40> Categories { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexerCategory40
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
||||||
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
||||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
||||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
|
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
|
||||||
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
||||||
Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
|
Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
|||||||
|
|
||||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { SearchTerm = "test", Categories = new[] { 2000, 5000 } })).Releases;
|
var releases = (await Subject.Fetch(new BasicSearchCriteria { SearchTerm = "test", Categories = new[] { 2000, 5000 } })).Releases;
|
||||||
|
|
||||||
releases.Should().HaveCount(33);
|
releases.Should().HaveCount(39);
|
||||||
releases.First().Should().BeOfType<TorrentInfo>();
|
releases.First().Should().BeOfType<TorrentInfo>();
|
||||||
|
|
||||||
var firstTorrentInfo = releases.ElementAt(2) as TorrentInfo;
|
var firstTorrentInfo = releases.ElementAt(3) as TorrentInfo;
|
||||||
|
|
||||||
firstTorrentInfo.Title.Should().Be("[SubsPlease] One Piece: The Great Gold Pirate - 1059 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 1059]");
|
firstTorrentInfo.Title.Should().Be("[SubsPlease] One Piece: The Great Gold Pirate - 1059 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 1059]");
|
||||||
firstTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
firstTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
|||||||
firstTorrentInfo.Files.Should().Be(1);
|
firstTorrentInfo.Files.Should().Be(1);
|
||||||
firstTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
firstTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
||||||
|
|
||||||
var secondTorrentInfo = releases.ElementAt(16) as TorrentInfo;
|
var secondTorrentInfo = releases.ElementAt(20) as TorrentInfo;
|
||||||
|
|
||||||
secondTorrentInfo.Title.Should().Be("[GHOST] BLEACH S03 [Blu-ray][MKV][h265 10-bit][1080p][AC3 2.0][Dual Audio][Softsubs (GHOST)]");
|
secondTorrentInfo.Title.Should().Be("[GHOST] BLEACH S03 [Blu-ray][MKV][h265 10-bit][1080p][AC3 2.0][Dual Audio][Softsubs (GHOST)]");
|
||||||
secondTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
secondTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
|||||||
secondTorrentInfo.Files.Should().Be(22);
|
secondTorrentInfo.Files.Should().Be(22);
|
||||||
secondTorrentInfo.MinimumSeedTime.Should().Be(655200);
|
secondTorrentInfo.MinimumSeedTime.Should().Be(655200);
|
||||||
|
|
||||||
var thirdTorrentInfo = releases.ElementAt(18) as TorrentInfo;
|
var thirdTorrentInfo = releases.ElementAt(23) as TorrentInfo;
|
||||||
|
|
||||||
thirdTorrentInfo.Title.Should().Be("[Polarwindz] Cowboy Bebop: Tengoku no Tobira 2001 [Blu-ray][MKV][h265 10-bit][1080p][Opus 5.1][Softsubs (Polarwindz)]");
|
thirdTorrentInfo.Title.Should().Be("[Polarwindz] Cowboy Bebop: Tengoku no Tobira 2001 [Blu-ray][MKV][h265 10-bit][1080p][Opus 5.1][Softsubs (Polarwindz)]");
|
||||||
thirdTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
thirdTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
@@ -102,7 +102,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
|||||||
thirdTorrentInfo.Files.Should().Be(1);
|
thirdTorrentInfo.Files.Should().Be(1);
|
||||||
thirdTorrentInfo.MinimumSeedTime.Should().Be(475200);
|
thirdTorrentInfo.MinimumSeedTime.Should().Be(475200);
|
||||||
|
|
||||||
var fourthTorrentInfo = releases.ElementAt(3) as TorrentInfo;
|
var fourthTorrentInfo = releases.ElementAt(5) as TorrentInfo;
|
||||||
|
|
||||||
fourthTorrentInfo.Title.Should().Be("[SubsPlease] Dr. STONE: NEW WORLD S03E03 - 03 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 3]");
|
fourthTorrentInfo.Title.Should().Be("[SubsPlease] Dr. STONE: NEW WORLD S03E03 - 03 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 3]");
|
||||||
fourthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
fourthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
@@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
|||||||
fourthTorrentInfo.Files.Should().Be(1);
|
fourthTorrentInfo.Files.Should().Be(1);
|
||||||
fourthTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
fourthTorrentInfo.MinimumSeedTime.Should().Be(259200);
|
||||||
|
|
||||||
var fifthTorrentInfo = releases.ElementAt(23) as TorrentInfo;
|
var fifthTorrentInfo = releases.ElementAt(28) as TorrentInfo;
|
||||||
|
|
||||||
fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]");
|
fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]");
|
||||||
fifthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
fifthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
@@ -138,7 +138,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
|||||||
fifthTorrentInfo.Files.Should().Be(11);
|
fifthTorrentInfo.Files.Should().Be(11);
|
||||||
fifthTorrentInfo.MinimumSeedTime.Should().Be(529200);
|
fifthTorrentInfo.MinimumSeedTime.Should().Be(529200);
|
||||||
|
|
||||||
var sixthTorrentInfo = releases.ElementAt(31) as TorrentInfo;
|
var sixthTorrentInfo = releases.ElementAt(37) as TorrentInfo;
|
||||||
|
|
||||||
sixthTorrentInfo.Title.Should().Be("[HorribleSubs] Dr. STONE S01 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]");
|
sixthTorrentInfo.Title.Should().Be("[HorribleSubs] Dr. STONE S01 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]");
|
||||||
sixthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
sixthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21:26:21"));
|
||||||
torrentInfo.Size.Should().Be(935127615);
|
torrentInfo.Size.Should().Be(935127615);
|
||||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 10:04:50"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 09:04:50"));
|
||||||
torrentInfo.Size.Should().Be(7085405541);
|
torrentInfo.Size.Should().Be(7085405541);
|
||||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49"));
|
||||||
torrentInfo.Size.Should().Be(69914591044);
|
torrentInfo.Size.Should().Be(69914591044);
|
||||||
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
+2
-2
@@ -114,7 +114,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
|
|||||||
query.Tvrage.Should().BeNull();
|
query.Tvrage.Should().BeNull();
|
||||||
query.Search.Should().BeNull();
|
query.Search.Should().BeNull();
|
||||||
query.Category.Should().Be("Episode");
|
query.Category.Should().Be("Episode");
|
||||||
query.Name.Should().Be("S01E03");
|
query.Name.Should().Be("S01E03%");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
|
|||||||
query.Tvrage.Should().BeNull();
|
query.Tvrage.Should().BeNull();
|
||||||
query.Search.Should().Be("Malcolm%in%the%Middle");
|
query.Search.Should().Be("Malcolm%in%the%Middle");
|
||||||
query.Category.Should().Be("Episode");
|
query.Category.Should().Be("Episode");
|
||||||
query.Name.Should().Be("S02E03");
|
query.Name.Should().Be("S02E03%");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
|
||||||
torrentInfo.Size.Should().Be(8300512414);
|
torrentInfo.Size.Should().Be(8300512414);
|
||||||
torrentInfo.InfoHash.Should().Be(null);
|
torrentInfo.InfoHash.Should().Be(null);
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
_caps = new IndexerCapabilities();
|
_caps = new IndexerCapabilities();
|
||||||
|
|
||||||
|
_caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies");
|
||||||
|
_caps.Categories.AddCategoryMapping(5000, NewznabStandardCategory.TV, "TV");
|
||||||
|
|
||||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||||
.Returns(_caps);
|
.Returns(_caps);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user