mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-13 20:46:53 -04:00
Compare commits
215 Commits
v0.3.22.24
...
v0.4.17.28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200ef600cd | ||
|
|
ad6228983b | ||
|
|
582ec9f7ce | ||
|
|
525e855038 | ||
|
|
7a629ed044 | ||
|
|
7f501322dd | ||
|
|
18bca0b228 | ||
|
|
9ddac60b47 | ||
|
|
bd8bc0b35b | ||
|
|
ae623f4481 | ||
|
|
e67d133bb6 | ||
|
|
772ea95ce4 | ||
|
|
5459a7bb7e | ||
|
|
614f98f9b4 | ||
|
|
55763dae43 | ||
|
|
a362dab503 | ||
|
|
dba9fbf254 | ||
|
|
59a7605385 | ||
|
|
f819e582cf | ||
|
|
0972d41bf8 | ||
|
|
05d2335bfe | ||
|
|
5b4f54a959 | ||
|
|
bb5ad605fd | ||
|
|
52c3a95e63 | ||
|
|
52c5460537 | ||
|
|
280cec3d0e | ||
|
|
f10c2c01d8 | ||
|
|
2b6a328dac | ||
|
|
4078525f67 | ||
|
|
214e4270ac | ||
|
|
5396dd3e8e | ||
|
|
d5d4996c40 | ||
|
|
8b2223a9c4 | ||
|
|
2a3e7f8dae | ||
|
|
6406ed6289 | ||
|
|
d5b0831b0f | ||
|
|
8d72d5dbab | ||
|
|
74d1ab84e2 | ||
|
|
7341d20c51 | ||
|
|
5abf4f2992 | ||
|
|
cc90050c77 | ||
|
|
7ba8f8baee | ||
|
|
70aec175ef | ||
|
|
080dd301f3 | ||
|
|
bab45481db | ||
|
|
3e1e03e0ce | ||
|
|
bb599d6dc9 | ||
|
|
c94685842f | ||
|
|
5966f6da51 | ||
|
|
229e0dfe5d | ||
|
|
5173daa265 | ||
|
|
c2f770f242 | ||
|
|
0b7ce67635 | ||
|
|
bc74456944 | ||
|
|
fa460567a7 | ||
|
|
7dfceb307b | ||
|
|
305ad235a5 | ||
|
|
74c20e41bf | ||
|
|
347289b173 | ||
|
|
0ef3d2a5cc | ||
|
|
e5519d60c9 | ||
|
|
3a85b3a060 | ||
|
|
c1cdf44322 | ||
|
|
f861e54139 | ||
|
|
279e1029e0 | ||
|
|
b9ed39175b | ||
|
|
faba3ada95 | ||
|
|
e8647aee05 | ||
|
|
eaf5ce52bc | ||
|
|
73ab2760e4 | ||
|
|
3bb036e8c6 | ||
|
|
6e05456d6a | ||
|
|
8563a42822 | ||
|
|
841d38f4a5 | ||
|
|
9326d88eb6 | ||
|
|
015da61004 | ||
|
|
d02ea4b121 | ||
|
|
7bc9d700f9 | ||
|
|
661d72ef9b | ||
|
|
258a8d1c95 | ||
|
|
d4459b9475 | ||
|
|
a550c6554f | ||
|
|
c1b26eec8d | ||
|
|
ffe5ede55d | ||
|
|
9005860899 | ||
|
|
c67f67109e | ||
|
|
51b9744e25 | ||
|
|
334d824633 | ||
|
|
ae01387ca9 | ||
|
|
4eb13e0938 | ||
|
|
6dbb826f2f | ||
|
|
52dfa57dd7 | ||
|
|
f354b3bc47 | ||
|
|
2d9e6788e6 | ||
|
|
0d121fe9c0 | ||
|
|
892c34fe35 | ||
|
|
24f6007594 | ||
|
|
5028ed4027 | ||
|
|
05f303436b | ||
|
|
5635de96a8 | ||
|
|
ce59f32023 | ||
|
|
6d675a5207 | ||
|
|
b093b23900 | ||
|
|
884ac2cb6f | ||
|
|
295a6c4255 | ||
|
|
74a59d5790 | ||
|
|
ae23e5f187 | ||
|
|
ba2add0d54 | ||
|
|
b6ebeb31c8 | ||
|
|
b8bd645560 | ||
|
|
e0d904fa69 | ||
|
|
cb532caca4 | ||
|
|
e1af8ad37f | ||
|
|
c4f30da648 | ||
|
|
b83a760873 | ||
|
|
22ab50f76d | ||
|
|
66758ca006 | ||
|
|
e7d7bc79f4 | ||
|
|
cfccb4f9c3 | ||
|
|
9312f17041 | ||
|
|
8192c22910 | ||
|
|
0b1d6b677a | ||
|
|
d666df0189 | ||
|
|
10d8f345c1 | ||
|
|
fb720b8714 | ||
|
|
e8131b5791 | ||
|
|
4f793f6b93 | ||
|
|
4215c21c94 | ||
|
|
6913789adc | ||
|
|
09e0c40792 | ||
|
|
baff805551 | ||
|
|
c885fe43cd | ||
|
|
464a777722 | ||
|
|
89e5999c85 | ||
|
|
b6fa332550 | ||
|
|
05f262dc0a | ||
|
|
699b765ee9 | ||
|
|
84beba2383 | ||
|
|
62eceb9148 | ||
|
|
f46070d4b0 | ||
|
|
73979c416a | ||
|
|
348e8f9c27 | ||
|
|
38bdb5a75d | ||
|
|
5e4c51e2f7 | ||
|
|
99a65246a9 | ||
|
|
598ce9a9d2 | ||
|
|
42d6b9e703 | ||
|
|
8f595838aa | ||
|
|
3d9d7d3582 | ||
|
|
77cf28bd78 | ||
|
|
2fb1b8af20 | ||
|
|
af1f389f8e | ||
|
|
b5334da253 | ||
|
|
68b3904382 | ||
|
|
c8b09b9e29 | ||
|
|
d910fc42ab | ||
|
|
a6db8bfe0e | ||
|
|
2033d7e411 | ||
|
|
4a04e54ceb | ||
|
|
d57a9ab9b0 | ||
|
|
d333204194 | ||
|
|
c3676f8d33 | ||
|
|
932356be61 | ||
|
|
5b1b2a2d67 | ||
|
|
c362e8c467 | ||
|
|
67c00a8cc7 | ||
|
|
27a086dfff | ||
|
|
8ee0df9c65 | ||
|
|
da30b55902 | ||
|
|
c7226fc85f | ||
|
|
84f22dbadc | ||
|
|
06a53ef9ca | ||
|
|
b5ef0cda1e | ||
|
|
1b1290efac | ||
|
|
dcbc3ea3f8 | ||
|
|
9a7b2cb818 | ||
|
|
f9cba39f0a | ||
|
|
6b6ff4fe76 | ||
|
|
05d0fe2da6 | ||
|
|
7aab2b49e2 | ||
|
|
8887df92ed | ||
|
|
9ee651d6c0 | ||
|
|
5544e169a6 | ||
|
|
11d83165e5 | ||
|
|
9e6d1c581c | ||
|
|
66e20a0aec | ||
|
|
e639b36283 | ||
|
|
c9f4fb141f | ||
|
|
29a43fc2fd | ||
|
|
f9454b5b5a | ||
|
|
9aa6d47349 | ||
|
|
e09946d946 | ||
|
|
c9c5429120 | ||
|
|
ed7bd6c66d | ||
|
|
c88fe7cae8 | ||
|
|
68642579d0 | ||
|
|
f061d70d38 | ||
|
|
fd4a609f51 | ||
|
|
9957f734a5 | ||
|
|
695b8b2ae1 | ||
|
|
420824b279 | ||
|
|
badc2567c3 | ||
|
|
c8c81927d9 | ||
|
|
f9df843789 | ||
|
|
3cd39d4ee8 | ||
|
|
8a39ef4c56 | ||
|
|
ba1195fc1b | ||
|
|
7656142db4 | ||
|
|
74c3b45ef8 | ||
|
|
f7368d3d09 | ||
|
|
5d8e2300f2 | ||
|
|
1fb54c0da5 | ||
|
|
5a9a6e593b | ||
|
|
2d5fc655c0 | ||
|
|
cfcc9a5856 |
13
.devcontainer/Readarr.code-workspace
Normal file
13
.devcontainer/Readarr.code-workspace
Normal file
@@ -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": {}
|
||||
}
|
||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -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": "Readarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8787],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -120,11 +120,13 @@ _artifacts
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -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 Readarr",
|
||||
"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/Readarr",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
.vscode/tasks.json
vendored
Normal file
44
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Readarr.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/Readarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Readarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,18 +9,18 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.3.22'
|
||||
majorVersion: '0.4.17'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
dotnetVersion: '6.0.427'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
linuxImage: 'ubuntu-22.04'
|
||||
macImage: 'macOS-13'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -166,10 +166,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -1075,10 +1075,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -1102,19 +1102,19 @@ stages:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@1
|
||||
- task: SonarCloudPrepare@3
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'readarr'
|
||||
scannerMode: 'CLI'
|
||||
scannerMode: 'cli'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'readarrui'
|
||||
cliProjectName: 'ReadarrUI'
|
||||
cliProjectVersion: '$(readarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@1
|
||||
- task: SonarCloudAnalyze@3
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
@@ -1190,12 +1190,12 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@1
|
||||
- task: SonarCloudPrepare@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'readarr'
|
||||
scannerMode: 'MSBuild'
|
||||
scannerMode: 'dotnet'
|
||||
projectKey: 'Readarr_Readarr'
|
||||
projectName: 'Readarr'
|
||||
projectVersion: '$(readarrVersion)'
|
||||
@@ -1208,21 +1208,16 @@ stages:
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
- task: SonarCloudAnalyze@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@4
|
||||
- task: reportgenerator@5.3.11
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: Publish Coverage Report
|
||||
inputs:
|
||||
codeCoverageTool: 'cobertura'
|
||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||
reportDirectory: './CoverageResults/combined/'
|
||||
publishCodeCoverageResults: true
|
||||
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
|
||||
14
docs.sh
14
docs.sh
@@ -1,3 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
@@ -21,15 +25,21 @@ slnFile=src/Readarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Readarr.Console.dll
|
||||
else
|
||||
application=Readarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/Readarr.console.dll" v1 &
|
||||
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
||||
@@ -26,6 +26,7 @@ module.exports = (env) => {
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
@@ -67,7 +68,7 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name]-[contenthash].js',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
@@ -92,7 +93,7 @@ module.exports = (env) => {
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -181,7 +182,7 @@ module.exports = (env) => {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
corejs: '3.39'
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -202,7 +203,7 @@ module.exports = (env) => {
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -165,7 +165,8 @@ function HistoryDetails(props) {
|
||||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message
|
||||
message,
|
||||
indexer
|
||||
} = data;
|
||||
|
||||
return (
|
||||
@@ -177,11 +178,21 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
indexer ?
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
message ?
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
@@ -247,7 +247,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={UpdatesConnector}
|
||||
component={Updates}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AuthorsAppState from './AuthorsAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
@@ -35,10 +36,24 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
authors: AuthorsAppState;
|
||||
commands: CommandAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
@@ -7,13 +8,16 @@ import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
@@ -33,11 +37,12 @@ export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
notifications: NotificationAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
13
frontend/src/App/State/SystemAppState.ts
Normal file
13
frontend/src/App/State/SystemAppState.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
@@ -1,5 +1,7 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export type AuthorStatus = 'continuing' | 'ended';
|
||||
|
||||
interface Author extends ModelBase {
|
||||
added: string;
|
||||
genres: string[];
|
||||
@@ -10,6 +12,7 @@ interface Author extends ModelBase {
|
||||
metadataProfileId: number;
|
||||
rootFolderPath: string;
|
||||
sortName: string;
|
||||
status: AuthorStatus;
|
||||
tags: number[];
|
||||
authorName: string;
|
||||
isSaving?: boolean;
|
||||
|
||||
21
frontend/src/Author/AuthorStatus.ts
Normal file
21
frontend/src/Author/AuthorStatus.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AuthorStatus } from 'Author/Author';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export function getAuthorStatusDetails(status: AuthorStatus) {
|
||||
let statusDetails = {
|
||||
icon: icons.AUTHOR_CONTINUING,
|
||||
title: translate('StatusEndedContinuing'),
|
||||
message: translate('ContinuingMoreBooksAreExpected'),
|
||||
};
|
||||
|
||||
if (status === 'ended') {
|
||||
statusDetails = {
|
||||
icon: icons.AUTHOR_ENDED,
|
||||
title: translate('StatusEndedEnded'),
|
||||
message: translate('ContinuingNoAdditionalBooksAreExpected'),
|
||||
};
|
||||
}
|
||||
|
||||
return statusDetails;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||
import Alert from 'Components/Alert';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -17,7 +18,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
@@ -412,22 +413,25 @@ class AuthorDetails extends Component {
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !booksError && !bookFilesError &&
|
||||
<LoadingIndicator />
|
||||
!isPopulated && !booksError && !bookFilesError ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && booksError &&
|
||||
<div>
|
||||
!isFetching && booksError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingBooksFailed')}
|
||||
</div>
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && bookFilesError &&
|
||||
<div>
|
||||
!isFetching && bookFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingBookFilesFailed')}
|
||||
</div>
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import AuthorPoster from 'Author/AuthorPoster';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
@@ -11,7 +12,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
@@ -87,11 +88,11 @@ class AuthorDetailsHeader extends Component {
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
||||
|
||||
const continuing = status === 'continuing';
|
||||
|
||||
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
||||
|
||||
if (bookFileCount === 1) {
|
||||
@@ -213,7 +214,7 @@ class AuthorDetailsHeader extends Component {
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
<QualityProfileName
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
@@ -236,16 +237,16 @@ class AuthorDetailsHeader extends Component {
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
|
||||
title={statusDetails.message}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
||||
name={statusDetails.icon}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{continuing ? 'Continuing' : 'Deceased'}
|
||||
{statusDetails.title}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
||||
import styles from './AuthorIndexOverviewInfo.css';
|
||||
|
||||
@@ -76,9 +77,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
title: translate('QualityProfile'),
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -235,12 +235,12 @@ class AuthorIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -209,7 +209,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile.name}
|
||||
{qualityProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -220,7 +220,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{metadataProfile.name}
|
||||
{metadataProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import Icon from 'Components/Icon';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -15,6 +16,8 @@ function AuthorStatusCell(props) {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
@@ -28,8 +31,8 @@ function AuthorStatusCell(props) {
|
||||
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
|
||||
name={statusDetails.icon}
|
||||
title={`${statusDetails.title}: ${statusDetails.message}`}
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './NoAuthor.css';
|
||||
|
||||
function NoAuthor(props) {
|
||||
@@ -31,7 +32,7 @@ function NoAuthor(props) {
|
||||
to="/settings/mediamanagement"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
Add Root Folder
|
||||
{translate('AddRootFolder')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +41,7 @@ function NoAuthor(props) {
|
||||
to="/add/search"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
Add New Author
|
||||
{translate('AddNewAuthor')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
||||
import styles from './BookIndexOverviewInfo.css';
|
||||
|
||||
@@ -71,9 +72,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
title: translate('QualityProfile'),
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile.name}
|
||||
{qualityProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||
import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookshelfBook from './BookshelfBook';
|
||||
import styles from './BookshelfRow.css';
|
||||
|
||||
@@ -30,6 +29,8 @@ class BookshelfRow extends Component {
|
||||
onBookMonitoredPress
|
||||
} = this.props;
|
||||
|
||||
const statusDetails = getAuthorStatusDetails(status);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
@@ -52,8 +53,8 @@ class BookshelfRow extends Component {
|
||||
<VirtualTableRowCell className={styles.status}>
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
||||
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
|
||||
name={statusDetails.icon}
|
||||
title={statusDetails.title}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './EnhancedSelectInput.css';
|
||||
|
||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||
|
||||
function isArrowKey(keyCode) {
|
||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||
}
|
||||
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
||||
// Listeners
|
||||
|
||||
onComputeMaxHeight = (data) => {
|
||||
const {
|
||||
top,
|
||||
bottom
|
||||
} = data.offsets.reference;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if ((/^botton/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom;
|
||||
} else {
|
||||
data.styles.maxHeight = top;
|
||||
}
|
||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
|
||||
order: 851,
|
||||
enabled: true,
|
||||
fn: this.onComputeMaxHeight
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type="password"
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
className: styles.input
|
||||
...TextInput.props
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
||||
@@ -28,8 +28,7 @@ function createMapStateToProps() {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: '',
|
||||
name: translate('NoChange'),
|
||||
value: translate('NoChange'),
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
isMissing: false
|
||||
});
|
||||
@@ -39,7 +38,6 @@ function createMapStateToProps() {
|
||||
values.push({
|
||||
key: '',
|
||||
value: '',
|
||||
name: '',
|
||||
isDisabled: true,
|
||||
isHidden: true
|
||||
});
|
||||
@@ -56,8 +54,7 @@ function createMapStateToProps() {
|
||||
|
||||
values.push({
|
||||
key: ADD_NEW_KEY,
|
||||
value: '',
|
||||
name: 'Add a new path'
|
||||
value: 'Add a new path'
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -105,6 +102,27 @@ class RootFolderSelectInputConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
values,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.values === values) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
|
||||
const defaultValue = values[0];
|
||||
|
||||
if (defaultValue.key !== ADD_NEW_KEY) {
|
||||
onChange({ name, value: defaultValue.key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.authorFolder {
|
||||
flex: 0 0 auto;
|
||||
color: var(--disabledColor);
|
||||
}
|
||||
|
||||
.freeSpace {
|
||||
margin-left: 15px;
|
||||
color: var(--darkGray);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'authorFolder': string;
|
||||
'freeSpace': string;
|
||||
'isMissing': string;
|
||||
'isMobile': string;
|
||||
'optionText': string;
|
||||
'value': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -7,18 +7,24 @@ import styles from './RootFolderSelectInputOption.css';
|
||||
|
||||
function RootFolderSelectInputOption(props) {
|
||||
const {
|
||||
id,
|
||||
value,
|
||||
name,
|
||||
freeSpace,
|
||||
authorFolder,
|
||||
isMissing,
|
||||
isMobile,
|
||||
isWindows,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const text = value === '' ? name : `${name} [${value}]`;
|
||||
const slashCharacter = isWindows ? '\\' : '/';
|
||||
|
||||
const text = name === '' ? value : `[${name}] ${value}`;
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
isMobile={isMobile}
|
||||
{...otherProps}
|
||||
>
|
||||
@@ -27,7 +33,18 @@ function RootFolderSelectInputOption(props) {
|
||||
isMobile && styles.isMobile
|
||||
)}
|
||||
>
|
||||
<div>{text}</div>
|
||||
<div className={styles.value}>
|
||||
{text}
|
||||
|
||||
{
|
||||
authorFolder && id !== 'addNew' ?
|
||||
<div className={styles.authorFolder}>
|
||||
{slashCharacter}
|
||||
{authorFolder}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
freeSpace == null ?
|
||||
@@ -50,11 +67,18 @@ function RootFolderSelectInputOption(props) {
|
||||
}
|
||||
|
||||
RootFolderSelectInputOption.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
authorFolder: PropTypes.string,
|
||||
isMissing: PropTypes.bool,
|
||||
isMobile: PropTypes.bool.isRequired
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool
|
||||
};
|
||||
|
||||
RootFolderSelectInputOption.defaultProps = {
|
||||
name: ''
|
||||
};
|
||||
|
||||
export default RootFolderSelectInputOption;
|
||||
|
||||
@@ -7,12 +7,22 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.path {
|
||||
.pathContainer {
|
||||
@add-mixin truncate;
|
||||
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.path {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.authorFolder {
|
||||
@add-mixin truncate;
|
||||
flex: 0 1 auto;
|
||||
color: var(--disabledColor);
|
||||
}
|
||||
|
||||
.freeSpace {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 15px;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'authorFolder': string;
|
||||
'freeSpace': string;
|
||||
'path': string;
|
||||
'pathContainer': string;
|
||||
'selectedValue': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -9,19 +9,34 @@ function RootFolderSelectInputSelectedValue(props) {
|
||||
name,
|
||||
value,
|
||||
freeSpace,
|
||||
authorFolder,
|
||||
includeFreeSpace,
|
||||
isWindows,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const text = value === '' ? name : `${name} [${value}]`;
|
||||
const slashCharacter = isWindows ? '\\' : '/';
|
||||
|
||||
const text = name === '' ? value : `[${name}] ${value}`;
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputSelectedValue
|
||||
className={styles.selectedValue}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.path}>
|
||||
{text}
|
||||
<div className={styles.pathContainer}>
|
||||
<div className={styles.path}>
|
||||
{text}
|
||||
</div>
|
||||
|
||||
{
|
||||
authorFolder ?
|
||||
<div className={styles.authorFolder}>
|
||||
{slashCharacter}
|
||||
{authorFolder}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -38,10 +53,13 @@ RootFolderSelectInputSelectedValue.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
freeSpace: PropTypes.number,
|
||||
authorFolder: PropTypes.string,
|
||||
isWindows: PropTypes.bool,
|
||||
includeFreeSpace: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
RootFolderSelectInputSelectedValue.defaultProps = {
|
||||
name: '',
|
||||
includeFreeSpace: true
|
||||
};
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class SelectInput extends Component {
|
||||
const {
|
||||
key,
|
||||
value: optionValue,
|
||||
isDisabled: optionIsDisabled = false,
|
||||
...otherOptionProps
|
||||
} = option;
|
||||
|
||||
@@ -59,6 +60,7 @@ class SelectInput extends Component {
|
||||
<option
|
||||
key={key}
|
||||
value={key}
|
||||
disabled={optionIsDisabled}
|
||||
{...otherOptionProps}
|
||||
>
|
||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||
|
||||
@@ -83,13 +83,6 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.modal.small,
|
||||
.modal.medium {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalContainer {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
&.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||
import SignalRConnector from 'Components/SignalRConnector';
|
||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import PageHeader from './Header/PageHeader';
|
||||
import PageSidebar from './Sidebar/PageSidebar';
|
||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
||||
isSmallScreen,
|
||||
isSidebarVisible,
|
||||
enableColorImpairedMode,
|
||||
authenticationEnabled,
|
||||
onSidebarToggle,
|
||||
onSidebarVisibleChange
|
||||
} = this.props;
|
||||
@@ -108,6 +110,10 @@ class Page extends Component {
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
|
||||
<AuthenticationRequiredModal
|
||||
isOpen={!authenticationEnabled}
|
||||
/>
|
||||
</div>
|
||||
</ColorImpairedContext.Provider>
|
||||
);
|
||||
@@ -123,6 +129,7 @@ Page.propTypes = {
|
||||
isUpdated: PropTypes.bool.isRequired,
|
||||
isDisconnected: PropTypes.bool.isRequired,
|
||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||
authenticationEnabled: PropTypes.bool.isRequired,
|
||||
onResize: PropTypes.func.isRequired,
|
||||
onSidebarToggle: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import ErrorPage from './ErrorPage';
|
||||
import LoadingPage from './LoadingPage';
|
||||
import Page from './Page';
|
||||
@@ -153,18 +154,21 @@ function createMapStateToProps() {
|
||||
selectErrors,
|
||||
selectAppProps,
|
||||
createDimensionsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
(
|
||||
enableColorImpairedMode,
|
||||
isPopulated,
|
||||
errors,
|
||||
app,
|
||||
dimensions
|
||||
dimensions,
|
||||
systemStatus
|
||||
) => {
|
||||
return {
|
||||
...app,
|
||||
...errors,
|
||||
isPopulated,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||
enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ class SignalRConnector extends Component {
|
||||
handleWantedCutoff = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({
|
||||
section: 'cutoffUnmet',
|
||||
section: 'wanted.cutoffUnmet',
|
||||
updateOnly: true,
|
||||
...body.resource
|
||||
});
|
||||
@@ -263,7 +263,7 @@ class SignalRConnector extends Component {
|
||||
handleWantedMissing = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({
|
||||
section: 'missing',
|
||||
section: 'wanted.missing',
|
||||
updateOnly: true,
|
||||
...body.resource
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
line-height: 1.52857143;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.tableContainer {
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.headerCell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.pager {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.headerCell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -25,14 +25,3 @@
|
||||
font-family: 'Ubuntu Mono';
|
||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* text-security-disc
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: 'text-security-disc';
|
||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
||||
@@ -0,0 +1,5 @@
|
||||
.authRequiredAlert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'input': string;
|
||||
'authRequiredAlert': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
170
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
170
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
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 { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
dispatchFetchStatus
|
||||
} = props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && didMount.current) {
|
||||
dispatchFetchStatus();
|
||||
}
|
||||
|
||||
didMount.current = true;
|
||||
}, [isSaving, dispatchFetchStatus]);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('AuthenticationRequired')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModalContent;
|
||||
@@ -0,0 +1,86 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(sectionSettings) => {
|
||||
return {
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchClearPendingChanges: clearPendingChanges,
|
||||
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||
dispatchFetchStatus: fetchStatus
|
||||
};
|
||||
|
||||
class AuthenticationRequiredModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveGeneralSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchFetchGeneralSettings,
|
||||
dispatchSetGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AuthenticationRequiredModalContent
|
||||
{...otherProps}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||
@@ -47,14 +47,16 @@ class InteractiveImportRow extends Component {
|
||||
author,
|
||||
book,
|
||||
foreignEditionId,
|
||||
quality
|
||||
quality,
|
||||
size
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
author &&
|
||||
book != null &&
|
||||
foreignEditionId &&
|
||||
quality
|
||||
quality &&
|
||||
size > 0
|
||||
) {
|
||||
this.props.onSelectedChange({ id, value: true });
|
||||
}
|
||||
|
||||
@@ -35,11 +35,12 @@
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
@@ -7,7 +8,7 @@ import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddNewAuthorSearchResultConnector from './Author/AddNewAuthorSearchResultConnector';
|
||||
@@ -81,7 +82,8 @@ class AddNewItem extends Component {
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
items
|
||||
items,
|
||||
hasExistingAuthors
|
||||
} = this.props;
|
||||
|
||||
const term = this.state.term;
|
||||
@@ -127,9 +129,16 @@ class AddNewItem extends Component {
|
||||
!isFetching && !!error ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Failed to load search results, please try again.
|
||||
{translate('FailedLoadingSearchResults')}
|
||||
</div>
|
||||
|
||||
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
|
||||
|
||||
<div>
|
||||
<Link to="https://wiki.servarr.com/readarr/troubleshooting#invalid-response-received-from-metadata-api">
|
||||
{translate('WhySearchesCouldBeFailing')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
@@ -178,7 +187,8 @@ class AddNewItem extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!term &&
|
||||
term ?
|
||||
null :
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
|
||||
@@ -191,6 +201,24 @@ class AddNewItem extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!term && !hasExistingAuthors ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noAuthorsText}>
|
||||
You haven't added any authors yet, do you want to add an existing library location (Root Folder) and update?
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
to="/settings/mediamanagement"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
{translate('AddRootFolder')}
|
||||
</Button>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
@@ -205,6 +233,7 @@ AddNewItem.propTypes = {
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasExistingAuthors: PropTypes.bool.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
onClearSearch: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -10,13 +10,15 @@ import AddNewItem from './AddNewItem';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.search,
|
||||
(state) => state.authors.items.length,
|
||||
(state) => state.router.location,
|
||||
(search, location) => {
|
||||
(search, existingAuthorsCount, location) => {
|
||||
const { params } = parseUrl(location.search);
|
||||
|
||||
return {
|
||||
...search,
|
||||
term: params.term,
|
||||
...search
|
||||
hasExistingAuthors: existingAuthorsCount > 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
||||
import styles from './AddNewAuthorModalContent.css';
|
||||
|
||||
@@ -54,7 +55,7 @@ class AddNewAuthorModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add new Author
|
||||
{translate('AddNewAuthor')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -133,7 +134,7 @@ class AddNewAuthorModalContent extends Component {
|
||||
|
||||
AddNewAuthorModalContent.propTypes = {
|
||||
authorName: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string,
|
||||
overview: PropTypes.string,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewAuthorModalContent from './AddNewAuthorModalContent';
|
||||
|
||||
@@ -12,7 +13,8 @@ function createMapStateToProps() {
|
||||
(state) => state.search,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createDimensionsSelector(),
|
||||
(searchState, metadataProfiles, dimensions) => {
|
||||
createSystemStatusSelector(),
|
||||
(searchState, metadataProfiles, dimensions, systemStatus) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
@@ -32,6 +34,7 @@ function createMapStateToProps() {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
isWindows: systemStatus.isWindows,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ class AddNewAuthorSearchResult extends Component {
|
||||
status,
|
||||
overview,
|
||||
ratings,
|
||||
folder,
|
||||
images,
|
||||
isExistingAuthor,
|
||||
isSmallScreen
|
||||
@@ -205,6 +206,7 @@ class AddNewAuthorSearchResult extends Component {
|
||||
disambiguation={disambiguation}
|
||||
year={year}
|
||||
overview={overview}
|
||||
folder={folder}
|
||||
images={images}
|
||||
onModalClose={this.onAddAuthorModalClose}
|
||||
/>
|
||||
@@ -222,6 +224,7 @@ AddNewAuthorSearchResult.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingAuthor: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
|
||||
@@ -10,6 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
||||
import styles from './AddNewBookModalContent.css';
|
||||
|
||||
@@ -58,7 +59,7 @@ class AddNewBookModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add new Book
|
||||
{translate('AddNewBook')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewBookModalContent from './AddNewBookModalContent';
|
||||
|
||||
@@ -13,7 +14,8 @@ function createMapStateToProps() {
|
||||
(state) => state.search,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createDimensionsSelector(),
|
||||
(isExistingAuthor, searchState, metadataProfiles, dimensions) => {
|
||||
createSystemStatusSelector(),
|
||||
(isExistingAuthor, searchState, metadataProfiles, dimensions, systemStatus) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
@@ -33,6 +35,7 @@ function createMapStateToProps() {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
isWindows: systemStatus.isWindows,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ class AddNewBookSearchResult extends Component {
|
||||
disambiguation={disambiguation}
|
||||
authorName={author.authorName}
|
||||
overview={overview}
|
||||
folder={author.folder}
|
||||
images={images}
|
||||
onModalClose={this.onAddBookModalClose}
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,9 @@ class AddAuthorOptionsForm extends Component {
|
||||
includeNoneMetadataProfile,
|
||||
includeSpecificBookMonitor,
|
||||
showMetadataProfile,
|
||||
folder,
|
||||
tags,
|
||||
isWindows,
|
||||
onInputChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -54,6 +56,15 @@ class AddAuthorOptionsForm extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
valueOptions={{
|
||||
authorFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
selectedValueOptions={{
|
||||
authorFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
helpText={translate('AddNewAuthorRootFolderHelpText', { folder })}
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
@@ -179,8 +190,14 @@ AddAuthorOptionsForm.propTypes = {
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
includeNoneMetadataProfile: PropTypes.bool.isRequired,
|
||||
includeSpecificBookMonitor: PropTypes.bool.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AddAuthorOptionsForm.defaultProps = {
|
||||
includeSpecificBookMonitor: false
|
||||
};
|
||||
|
||||
export default AddAuthorOptionsForm;
|
||||
|
||||
@@ -25,3 +25,8 @@
|
||||
border-radius: 4px;
|
||||
background-color: var(--cardCenterBackgroundColor);
|
||||
}
|
||||
|
||||
.customFormats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
interface CssExports {
|
||||
'addSpecification': string;
|
||||
'center': string;
|
||||
'customFormats': string;
|
||||
'deleteButton': string;
|
||||
'rightButtons': string;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteDownloadClients,
|
||||
bulkEditDownloadClients,
|
||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||
typeof ManageDownloadClientsModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
@@ -82,8 +82,6 @@ const COLUMNS = [
|
||||
|
||||
interface ManageDownloadClientsModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageDownloadClientsModalContent(
|
||||
|
||||
@@ -11,16 +11,69 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None' },
|
||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||
export const authenticationMethodOptions = [
|
||||
{
|
||||
key: 'none',
|
||||
get value() {
|
||||
return translate('None');
|
||||
},
|
||||
isDisabled: true
|
||||
},
|
||||
{
|
||||
key: 'external',
|
||||
get value() {
|
||||
return translate('External');
|
||||
},
|
||||
isHidden: true
|
||||
},
|
||||
{
|
||||
key: 'basic',
|
||||
get value() {
|
||||
return translate('AuthBasic');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'forms',
|
||||
get value() {
|
||||
return translate('AuthForm');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const authenticationRequiredOptions = [
|
||||
{
|
||||
key: 'enabled',
|
||||
get value() {
|
||||
return translate('Enabled');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disabledForLocalAddresses',
|
||||
get value() {
|
||||
return translate('DisabledForLocalAddresses');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const certificateValidationOptions = [
|
||||
{ key: 'enabled', value: 'Enabled' },
|
||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
||||
{ key: 'disabled', value: 'Disabled' }
|
||||
{
|
||||
key: 'enabled',
|
||||
get value() {
|
||||
return translate('Enabled');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disabledForLocalAddresses',
|
||||
get value() {
|
||||
return translate('DisabledForLocalAddresses');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disabled',
|
||||
get value() {
|
||||
return translate('Disabled');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class SecuritySettings extends Component {
|
||||
@@ -68,8 +121,10 @@ class SecuritySettings extends Component {
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
apiKey,
|
||||
certificateValidation
|
||||
} = settings;
|
||||
@@ -79,26 +134,40 @@ class SecuritySettings extends Component {
|
||||
return (
|
||||
<FieldSet legend={translate('Security')}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Authentication')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={translate('AuthenticationRequiredWarning')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Username')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
@@ -106,15 +175,14 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Password')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
@@ -122,19 +190,33 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('APIKey')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('ApiKey')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="apiKey"
|
||||
readOnly={true}
|
||||
helpTextWarning={translate('ApiKeyHelpTextWarning')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
buttons={[
|
||||
<ClipboardButton
|
||||
key="copy"
|
||||
@@ -160,9 +242,7 @@ class SecuritySettings extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('CertificateValidation')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('CertificateValidation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
|
||||
@@ -18,7 +18,6 @@ function UpdateSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isWindows,
|
||||
packageUpdateMechanism,
|
||||
onInputChange
|
||||
} = props;
|
||||
@@ -44,10 +43,10 @@ function UpdateSettings(props) {
|
||||
value: titleCase(packageUpdateMechanism)
|
||||
});
|
||||
} else {
|
||||
updateOptions.push({ key: 'builtIn', value: 'Built-In' });
|
||||
updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') });
|
||||
}
|
||||
|
||||
updateOptions.push({ key: 'script', value: 'Script' });
|
||||
updateOptions.push({ key: 'script', value: translate('Script') });
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Updates')}>
|
||||
@@ -60,8 +59,8 @@ function UpdateSettings(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.AUTO_COMPLETE}
|
||||
name="branch"
|
||||
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
|
||||
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
||||
helpLink="https://wiki.servarr.com/readarr/settings#updates"
|
||||
{...branch}
|
||||
values={branchValues}
|
||||
onChange={onInputChange}
|
||||
@@ -69,62 +68,59 @@ function UpdateSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
!isWindows &&
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/readarr/settings#updates"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-i-update-my-readarr"
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ function EditImportListExclusionModalContent(props) {
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MusicbrainzId')}
|
||||
{translate('ForeignId')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
|
||||
@@ -8,6 +8,7 @@ import TagListConnector from 'Components/TagListConnector';
|
||||
import { createMetadataProfileSelectorForHook } from 'Store/Selectors/createMetadataProfileSelector';
|
||||
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ManageImportListsModalRow.css';
|
||||
|
||||
interface ManageImportListsModalRowProps {
|
||||
@@ -70,7 +71,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.qualityProfileId}>
|
||||
{qualityProfile?.name ?? 'None'}
|
||||
{qualityProfile?.name ?? translate('None')}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.metadataProfileId}>
|
||||
@@ -82,7 +83,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.enableAutomaticAdd}>
|
||||
{enableAutomaticAdd ? 'Yes' : 'No'}
|
||||
{enableAutomaticAdd ? translate('Yes') : translate('No')}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.tags}>
|
||||
|
||||
@@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteIndexers,
|
||||
bulkEditIndexers,
|
||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||
typeof ManageIndexersModalRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: () => translate('Name'),
|
||||
@@ -82,8 +82,6 @@ const COLUMNS = [
|
||||
|
||||
interface ManageIndexersModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -15,11 +14,11 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.namingExamples,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, examples, sectionSettings) => {
|
||||
(advancedSettings, namingExamples, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
examples: examples.item,
|
||||
examplesPopulated: !_.isEmpty(examples.item),
|
||||
examples: namingExamples.item,
|
||||
examplesPopulated: namingExamples.isPopulated,
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,12 +75,12 @@ class RootFolder extends Component {
|
||||
{path}
|
||||
</Label>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{qualityProfile.name}
|
||||
<Label kind={qualityProfile?.name ? kinds.SUCCESS : kinds.DANGER}>
|
||||
{qualityProfile?.name || translate('None')}
|
||||
</Label>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{metadataProfile.name}
|
||||
<Label kind={metadataProfile?.name ? kinds.SUCCESS : kinds.DANGER}>
|
||||
{metadataProfile?.name || translate('None')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface QualityProfileNameProps {
|
||||
qualityProfileId: number;
|
||||
}
|
||||
|
||||
function QualityProfileName({ qualityProfileId }: QualityProfileNameProps) {
|
||||
const qualityProfile = useSelector(
|
||||
createQualityProfileSelectorForHook(qualityProfileId)
|
||||
);
|
||||
|
||||
return <span>{qualityProfile?.name ?? translate('Unknown')}</span>;
|
||||
}
|
||||
|
||||
export default QualityProfileName;
|
||||
@@ -1,31 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createQualityProfileSelector(),
|
||||
(qualityProfile) => {
|
||||
return {
|
||||
name: qualityProfile.name
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function QualityProfileNameConnector({ name, ...otherProps }) {
|
||||
return (
|
||||
<span>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
QualityProfileNameConnector.propTypes = {
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(QualityProfileNameConnector);
|
||||
@@ -1,8 +1,11 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastTestData = null;
|
||||
|
||||
export function createCancelTestProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
@@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
||||
return function(getState, payload, dispatch) {
|
||||
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 = {
|
||||
url: `${url}/test`,
|
||||
url: `${url}/test?${$.param(params, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
@@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
||||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastTestData = null;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isTesting: false,
|
||||
|
||||
@@ -45,6 +45,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'books.lastSearchTime',
|
||||
label: 'Last Searched',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: 'Actions',
|
||||
@@ -108,6 +114,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'books.lastSearchTime',
|
||||
label: 'Last Searched',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: 'Actions',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createTagsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.tags.items,
|
||||
(state: AppState) => state.tags.items,
|
||||
(tags) => {
|
||||
return tags;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ module.exports = {
|
||||
// Families
|
||||
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
|
||||
passwordFamily: 'text-security-disc',
|
||||
|
||||
// Sizes
|
||||
extraSmallFontSize: '11px',
|
||||
|
||||
@@ -6,6 +6,22 @@ import createMultiAuthorsSelector from 'Store/Selectors/createMultiAuthorsSelect
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRowNameCell.css';
|
||||
|
||||
function formatTitles(titles: string[]) {
|
||||
if (!titles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (titles.length > 11) {
|
||||
return (
|
||||
<span title={titles.join(', ')}>
|
||||
{titles.slice(0, 10).join(', ')}, {titles.length - 10} more
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{titles.join(', ')}</span>;
|
||||
}
|
||||
|
||||
export interface QueuedTaskRowNameCellProps {
|
||||
commandName: string;
|
||||
body: CommandBody;
|
||||
@@ -32,7 +48,7 @@ export default function QueuedTaskRowNameCell(
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
{sortedAuthors.length ? (
|
||||
<span> - {sortedAuthors.map((a) => a.authorName).join(', ')}</span>
|
||||
<span> - {formatTitles(sortedAuthors.map((a) => a.authorName))}</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
class UpdateChanges extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
changes
|
||||
} = this.props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{
|
||||
changes.map((change, index) => {
|
||||
const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
|
||||
return `[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(1)})`;
|
||||
});
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UpdateChanges.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
changes: PropTypes.arrayOf(PropTypes.string)
|
||||
};
|
||||
|
||||
export default UpdateChanges;
|
||||
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal file
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
interface UpdateChangesProps {
|
||||
title: string;
|
||||
changes: string[];
|
||||
}
|
||||
|
||||
function UpdateChanges(props: UpdateChangesProps) {
|
||||
const { title, changes } = props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueChanges = [...new Set(changes)];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{uniqueChanges.map((change, index) => {
|
||||
const checkChange = change.replace(
|
||||
/#\d{4,5}\b/g,
|
||||
(match) =>
|
||||
`[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(
|
||||
1
|
||||
)})`
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdateChanges;
|
||||
@@ -1,252 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
class Updates extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentVersion,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
isInstallingUpdate,
|
||||
updateMechanism,
|
||||
isDocker,
|
||||
updateMechanismMessage,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onInstallLatestPress
|
||||
} = this.props;
|
||||
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const externalUpdaterPrefix = 'Unable to update Readarr directly,';
|
||||
const externalUpdaterMessages = {
|
||||
external: 'Readarr is configured to use an external update mechanism',
|
||||
apt: 'use apt to install the update',
|
||||
docker: 'update the docker container to receive the update'
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{
|
||||
!isPopulated && !hasError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
noUpdates &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoUpdatesAreAvailable')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
{
|
||||
(updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ?
|
||||
<SpinnerButton
|
||||
className={styles.updateAvailable}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={onInstallLatestPress}
|
||||
>
|
||||
Install Latest
|
||||
</SpinnerButton> :
|
||||
|
||||
<Fragment>
|
||||
<Icon
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
noUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
The latest version of Readarr is already installed
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdates &&
|
||||
<div>
|
||||
{
|
||||
items.map((update) => {
|
||||
const hasChanges = !!update.changes;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={update.version}
|
||||
className={styles.update}
|
||||
>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{
|
||||
update.branch === 'master' ?
|
||||
null :
|
||||
<Label
|
||||
className={styles.label}
|
||||
>
|
||||
{update.branch}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
update.version === currentVersion ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Currently Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
update.version !== currentVersion && update.installedOn ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Previously Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!hasChanges &&
|
||||
<div>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasChanges &&
|
||||
<div className={styles.changes}>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!updatesError &&
|
||||
<div>
|
||||
Failed to fetch updates
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!generalSettingsError &&
|
||||
<div>
|
||||
Failed to update settings
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Updates.propTypes = {
|
||||
currentVersion: PropTypes.string.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
updatesError: PropTypes.object,
|
||||
generalSettingsError: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
isInstallingUpdate: PropTypes.bool.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
updateMechanism: PropTypes.string,
|
||||
updateMechanismMessage: PropTypes.string,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onInstallLatestPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Updates;
|
||||
303
frontend/src/System/Updates/Updates.tsx
Normal file
303
frontend/src/System/Updates/Updates.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { UpdateMechanism } from 'typings/Settings/General';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
||||
|
||||
function createUpdatesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.system.updates,
|
||||
(state: AppState) => state.settings.general,
|
||||
(updates, generalSettings) => {
|
||||
const { error: updatesError, items } = updates;
|
||||
|
||||
const isFetching = updates.isFetching || generalSettings.isFetching;
|
||||
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError: generalSettings.error,
|
||||
items,
|
||||
updateMechanism: generalSettings.item.updateMechanism,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function Updates() {
|
||||
const currentVersion = useSelector((state: AppState) => state.app.version);
|
||||
const { packageUpdateMechanismMessage } = useSelector(
|
||||
createSystemStatusSelector()
|
||||
);
|
||||
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
const isInstallingUpdate = useSelector(
|
||||
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
|
||||
);
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
updateMechanism,
|
||||
} = useSelector(createUpdatesSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
|
||||
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
|
||||
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
|
||||
external: translate('ExternalUpdater'),
|
||||
apt: translate('AptUpdater'),
|
||||
docker: translate('DockerUpdater'),
|
||||
};
|
||||
|
||||
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
|
||||
const majorVersion = parseInt(
|
||||
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
const latestVersion = items[0]?.version;
|
||||
const latestMajorVersion = parseInt(
|
||||
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
return {
|
||||
isMajorUpdate: latestMajorVersion > majorVersion,
|
||||
hasUpdateToInstall: items.some(
|
||||
(update) => update.installable && update.latest
|
||||
),
|
||||
};
|
||||
}, [currentVersion, items]);
|
||||
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const handleInstallLatestPress = useCallback(() => {
|
||||
if (isMajorUpdate) {
|
||||
setIsMajorUpdateModalOpen(true);
|
||||
} else {
|
||||
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
|
||||
}
|
||||
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleInstallLatestMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.APPLICATION_UPDATE,
|
||||
installMajorUpdate: true,
|
||||
})
|
||||
);
|
||||
}, [setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleCancelMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
}, [setIsMajorUpdateModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUpdates());
|
||||
dispatch(fetchGeneralSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{isPopulated || hasError ? null : <LoadingIndicator />}
|
||||
|
||||
{noUpdates ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
|
||||
) : null}
|
||||
|
||||
{hasUpdateToInstall ? (
|
||||
<div className={styles.messageContainer}>
|
||||
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={handleInstallLatestPress}
|
||||
>
|
||||
{translate('InstallLatest')}
|
||||
</SpinnerButton>
|
||||
) : (
|
||||
<>
|
||||
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix}{' '}
|
||||
<InlineMarkdown
|
||||
data={
|
||||
packageUpdateMechanismMessage ||
|
||||
externalUpdaterMessages[updateMechanism] ||
|
||||
externalUpdaterMessages.external
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isFetching ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{noUpdateToInstall && (
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
<div className={styles.message}>{translate('OnLatestVersion')}</div>
|
||||
|
||||
{isFetching && (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasUpdates && (
|
||||
<div>
|
||||
{items.map((update) => {
|
||||
return (
|
||||
<div key={update.version} className={styles.update}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(
|
||||
update.releaseDate,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{update.branch === 'master' ? null : (
|
||||
<Label className={styles.label}>{update.branch}</Label>
|
||||
)}
|
||||
|
||||
{update.version === currentVersion ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('CurrentlyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
|
||||
{update.version !== currentVersion && update.installedOn ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('PreviouslyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{update.changes ? (
|
||||
<div>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>{translate('MaintenanceRelease')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatesError ? (
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('FailedToFetchUpdates')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{generalSettingsError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('FailedToFetchSettings')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMajorUpdateModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('InstallMajorVersionUpdate')}
|
||||
message={
|
||||
<div>
|
||||
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('InstallMajorVersionUpdateMessageLink', {
|
||||
domain: 'readarr.com',
|
||||
url: 'https://readarr.com/#downloads',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Install')}
|
||||
onConfirm={handleInstallLatestMajorVersionPress}
|
||||
onCancel={handleCancelMajorVersionPress}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Updates;
|
||||
@@ -27,6 +27,12 @@ export default function translate(
|
||||
key: string,
|
||||
tokens: Record<string, string | number | boolean> = {}
|
||||
) {
|
||||
const { isProduction = true } = window.Readarr;
|
||||
|
||||
if (!isProduction && !(key in translations)) {
|
||||
console.warn(`Missing translation for key: ${key}`);
|
||||
}
|
||||
|
||||
const translation = translations[key] || key;
|
||||
|
||||
tokens.appName = 'Readarr';
|
||||
|
||||
@@ -131,13 +131,15 @@ class CutoffUnmetConnector extends Component {
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.BOOK_SEARCH,
|
||||
bookIds: selected
|
||||
bookIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH
|
||||
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ function CutoffUnmetRow(props) {
|
||||
releaseDate,
|
||||
titleSlug,
|
||||
title,
|
||||
lastSearchTime,
|
||||
disambiguation,
|
||||
isSelected,
|
||||
columns,
|
||||
@@ -68,6 +69,15 @@ function CutoffUnmetRow(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'books.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseDate') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
@@ -105,6 +115,7 @@ CutoffUnmetRow.propTypes = {
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
disambiguation: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -121,13 +121,15 @@ class MissingConnector extends Component {
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.BOOK_SEARCH,
|
||||
bookIds: selected
|
||||
bookIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllMissingPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MISSING_BOOK_SEARCH
|
||||
name: commandNames.MISSING_BOOK_SEARCH,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ function MissingRow(props) {
|
||||
releaseDate,
|
||||
titleSlug,
|
||||
title,
|
||||
lastSearchTime,
|
||||
disambiguation,
|
||||
isSelected,
|
||||
columns,
|
||||
@@ -77,6 +78,15 @@ function MissingRow(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'books.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<BookSearchCellConnector
|
||||
@@ -104,6 +114,7 @@ MissingRow.propTypes = {
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
disambiguation: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
45
frontend/src/typings/Settings/General.ts
Normal file
45
frontend/src/typings/Settings/General.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export type UpdateMechanism =
|
||||
| 'builtIn'
|
||||
| 'script'
|
||||
| 'external'
|
||||
| 'apt'
|
||||
| 'docker';
|
||||
|
||||
export default interface General {
|
||||
bindAddress: string;
|
||||
port: number;
|
||||
sslPort: number;
|
||||
enableSsl: boolean;
|
||||
launchBrowser: boolean;
|
||||
authenticationMethod: string;
|
||||
authenticationRequired: string;
|
||||
analyticsEnabled: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
logLevel: string;
|
||||
consoleLogLevel: string;
|
||||
branch: string;
|
||||
apiKey: string;
|
||||
sslCertPath: string;
|
||||
sslCertPassword: string;
|
||||
urlBase: string;
|
||||
instanceName: string;
|
||||
applicationUrl: string;
|
||||
updateAutomatically: boolean;
|
||||
updateMechanism: UpdateMechanism;
|
||||
updateScriptPath: string;
|
||||
proxyEnabled: boolean;
|
||||
proxyType: string;
|
||||
proxyHostname: string;
|
||||
proxyPort: number;
|
||||
proxyUsername: string;
|
||||
proxyPassword: string;
|
||||
proxyBypassFilter: string;
|
||||
proxyBypassLocalAddresses: boolean;
|
||||
certificateValidation: string;
|
||||
backupFolder: string;
|
||||
backupInterval: number;
|
||||
backupRetention: number;
|
||||
id: number;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface UiSettings {
|
||||
export default interface UiSettings {
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
32
frontend/src/typings/SystemStatus.ts
Normal file
32
frontend/src/typings/SystemStatus.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
interface SystemStatus {
|
||||
appData: string;
|
||||
appName: string;
|
||||
authentication: string;
|
||||
branch: string;
|
||||
buildTime: string;
|
||||
instanceName: string;
|
||||
isAdmin: boolean;
|
||||
isDebug: boolean;
|
||||
isDocker: boolean;
|
||||
isLinux: boolean;
|
||||
isNetCore: boolean;
|
||||
isOsx: boolean;
|
||||
isProduction: boolean;
|
||||
isUserInteractive: boolean;
|
||||
isWindows: boolean;
|
||||
migrationVersion: number;
|
||||
mode: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
packageUpdateMechanism: string;
|
||||
packageUpdateMechanismMessage: string;
|
||||
runtimeName: string;
|
||||
runtimeVersion: string;
|
||||
sqliteVersion: string;
|
||||
startTime: string;
|
||||
startupPath: string;
|
||||
urlBase: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export default SystemStatus;
|
||||
20
frontend/src/typings/Update.ts
Normal file
20
frontend/src/typings/Update.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface Changes {
|
||||
new: string[];
|
||||
fixed: string[];
|
||||
}
|
||||
|
||||
interface Update {
|
||||
version: string;
|
||||
branch: string;
|
||||
releaseDate: string;
|
||||
fileName: string;
|
||||
url: string;
|
||||
installed: boolean;
|
||||
installedOn: string;
|
||||
installable: boolean;
|
||||
latest: boolean;
|
||||
changes: Changes | null;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export default Update;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user