mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Compare commits
256 Commits
phantom-jo
...
phantom-ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
808836a65f | ||
|
|
6f7e505bed | ||
|
|
ecf1d75954 | ||
|
|
c5257de436 | ||
|
|
cde5a6d1a4 | ||
|
|
b601c8bcfe | ||
|
|
023c8260f2 | ||
|
|
51e2e084af | ||
|
|
fc5dd8137f | ||
|
|
268fc46ef7 | ||
|
|
010c65af9c | ||
|
|
db42256dc3 | ||
|
|
e9b537b6e6 | ||
|
|
c615ef476a | ||
|
|
b93e8da235 | ||
|
|
74a0a57468 | ||
|
|
b19d665817 | ||
|
|
10dc884fa8 | ||
|
|
d8446c2d5a | ||
|
|
d3cd46bb51 | ||
|
|
bc0da03caf | ||
|
|
c0a356261b | ||
|
|
fa4060b7fe | ||
|
|
29117fc222 | ||
|
|
24ba5e5bda | ||
|
|
2d94857369 | ||
|
|
c6ea7d7e63 | ||
|
|
3916495329 | ||
|
|
4e965e59a9 | ||
|
|
0acb3aa32b | ||
|
|
9189d8bf4d | ||
|
|
ec0c96bde4 | ||
|
|
562c8c4afe | ||
|
|
fd6d4493c4 | ||
|
|
1a2419e096 | ||
|
|
b86cfd49ef | ||
|
|
d421ff9736 | ||
|
|
92c61701f2 | ||
|
|
d45d9e356c | ||
|
|
098f9a2675 | ||
|
|
0347dab82e | ||
|
|
9aa89a0df9 | ||
|
|
556bd11725 | ||
|
|
07f5c21a07 | ||
|
|
93b20960b8 | ||
|
|
3cbdd6bfd3 | ||
|
|
c3c38880e6 | ||
|
|
415bbf5b3b | ||
|
|
186cb02748 | ||
|
|
4aaccb909f | ||
|
|
2daf7dd01a | ||
|
|
ab9ed73e55 | ||
|
|
a4a33fe167 | ||
|
|
e6fbd10031 | ||
|
|
9868d96fec | ||
|
|
0d1c2ac40c | ||
|
|
a6d0dddaf7 | ||
|
|
06d57e8f32 | ||
|
|
70a40edc5d | ||
|
|
95d64208d0 | ||
|
|
e28b2e8328 | ||
|
|
4123745a6b | ||
|
|
70bb4d71e6 | ||
|
|
dd314e1741 | ||
|
|
3cbb489ac6 | ||
|
|
101df4cbf1 | ||
|
|
42263a0ec0 | ||
|
|
d402f7514e | ||
|
|
84e6674e23 | ||
|
|
afcfaace19 | ||
|
|
5a3bd8cfe5 | ||
|
|
f0c90a4744 | ||
|
|
3baed292e1 | ||
|
|
d41a2cad73 | ||
|
|
ffccc3be38 | ||
|
|
ef1e8d7ef3 | ||
|
|
7af891216d | ||
|
|
aa80500b35 | ||
|
|
b72fbe06f7 | ||
|
|
41a63a5418 | ||
|
|
e8ce7898c1 | ||
|
|
687a45c564 | ||
|
|
3ac3dd3ca5 | ||
|
|
f2efebf7d9 | ||
|
|
7b68ce49d5 | ||
|
|
8a2a41fab0 | ||
|
|
ceaaec5378 | ||
|
|
e49a3e7206 | ||
|
|
dc7986dbad | ||
|
|
2dfba130f5 | ||
|
|
155c7c409b | ||
|
|
aacb8970f8 | ||
|
|
be66a0520d | ||
|
|
3fa3c45794 | ||
|
|
0f6da1873e | ||
|
|
1564208e83 | ||
|
|
e724e8db60 | ||
|
|
5a092a83cd | ||
|
|
ffefe5e8aa | ||
|
|
631fdd8a26 | ||
|
|
53d7ef4014 | ||
|
|
5c3ac79043 | ||
|
|
90fb1646e0 | ||
|
|
54604e45e0 | ||
|
|
9ed0f9eee8 | ||
|
|
b764c44318 | ||
|
|
adbd519061 | ||
|
|
b0415299ca | ||
|
|
e96d05149c | ||
|
|
354ddcfee5 | ||
|
|
6d232778e2 | ||
|
|
95ee7daf21 | ||
|
|
2238ac5d17 | ||
|
|
c209c1c034 | ||
|
|
b1eec16333 | ||
|
|
e126c45fb3 | ||
|
|
c89ff93be4 | ||
|
|
c82c27a5c5 | ||
|
|
b3e84f407a | ||
|
|
72902c8984 | ||
|
|
2c47c5eb99 | ||
|
|
398129f3e1 | ||
|
|
d74ab12d9e | ||
|
|
679c0599dd | ||
|
|
4d04ad5632 | ||
|
|
3fdc50b354 | ||
|
|
7eeff32185 | ||
|
|
d40f2cb852 | ||
|
|
f9dc2fb6d5 | ||
|
|
de31dfb11e | ||
|
|
ef6a648189 | ||
|
|
09953e2af8 | ||
|
|
be240119e8 | ||
|
|
2b7893c834 | ||
|
|
896e824ca1 | ||
|
|
7a94725808 | ||
|
|
a66fb76e9a | ||
|
|
b453d48fee | ||
|
|
a7f2c07998 | ||
|
|
3cff878f74 | ||
|
|
665d536481 | ||
|
|
ec6d407fbb | ||
|
|
72bc7ed6d4 | ||
|
|
ac407ca2c0 | ||
|
|
6af5f2b528 | ||
|
|
8fd4a98fbe | ||
|
|
07d553fae3 | ||
|
|
34e0eea173 | ||
|
|
73e6db9a12 | ||
|
|
0df464ac03 | ||
|
|
78ee6afbae | ||
|
|
31be74e6d3 | ||
|
|
767a09894a | ||
|
|
0d410d107d | ||
|
|
7829b18b3c | ||
|
|
b2267a55ce | ||
|
|
eca016fe61 | ||
|
|
81723f7fa9 | ||
|
|
44c91fb90c | ||
|
|
7cb5bd9c95 | ||
|
|
8196f6b9db | ||
|
|
d72b16531b | ||
|
|
f333196efe | ||
|
|
6ea047dcb4 | ||
|
|
ea65867b23 | ||
|
|
c41200e762 | ||
|
|
4c70afbb53 | ||
|
|
0c1ce66053 | ||
|
|
fa8b8cebf9 | ||
|
|
72fa89ba76 | ||
|
|
dc7b4cebf2 | ||
|
|
3c1dd94915 | ||
|
|
3decbbac3a | ||
|
|
27f43569f5 | ||
|
|
bd9bded73b | ||
|
|
0ce81e1ab6 | ||
|
|
2926201694 | ||
|
|
059be2c853 | ||
|
|
dd09f31abb | ||
|
|
1da20da3ff | ||
|
|
341773830b | ||
|
|
c65452bb01 | ||
|
|
34d81356a3 | ||
|
|
c9b84a5202 | ||
|
|
5394cc2dc9 | ||
|
|
63141f339f | ||
|
|
079a0b56c3 | ||
|
|
66721affe7 | ||
|
|
d8ab23e9ba | ||
|
|
11b0a5c9cc | ||
|
|
d273a72cb3 | ||
|
|
08641a6694 | ||
|
|
9ac5c0d886 | ||
|
|
8a57d33223 | ||
|
|
1945b53e43 | ||
|
|
161e1820a8 | ||
|
|
22b5ac8622 | ||
|
|
f75a70e42b | ||
|
|
c9076606be | ||
|
|
853d22b947 | ||
|
|
82d6719d91 | ||
|
|
3f02c150e3 | ||
|
|
d7d46a93a7 | ||
|
|
dfd51635a6 | ||
|
|
c04366d505 | ||
|
|
fd89e88d40 | ||
|
|
894de923b9 | ||
|
|
c47e7cd91d | ||
|
|
e359347a3b | ||
|
|
d320017e3c | ||
|
|
e6c34f4311 | ||
|
|
9b0c945086 | ||
|
|
bc85f5de1d | ||
|
|
4df219161c | ||
|
|
06f157e634 | ||
|
|
54addbdd28 | ||
|
|
0e721917e7 | ||
|
|
4dc7089f89 | ||
|
|
30b5a35db2 | ||
|
|
dc8f81b536 | ||
|
|
a018770a18 | ||
|
|
5e4f7c5d8e | ||
|
|
1d9d665ed0 | ||
|
|
68477c09a7 | ||
|
|
574b9086d4 | ||
|
|
d74c323c66 | ||
|
|
8e85a1b84e | ||
|
|
6ce1cb4325 | ||
|
|
fbbd85d8b2 | ||
|
|
e611dc43c5 | ||
|
|
c86309cfc0 | ||
|
|
ec74e9bce0 | ||
|
|
f371e8a523 | ||
|
|
57a059eecb | ||
|
|
2cb149c647 | ||
|
|
ee5371b582 | ||
|
|
70e4dbe3bd | ||
|
|
18ead9a64f | ||
|
|
d2764cee2a | ||
|
|
082c098420 | ||
|
|
46a42e2901 | ||
|
|
c21cacd309 | ||
|
|
81ac359f71 | ||
|
|
f5b91c90bc | ||
|
|
0a92a3012e | ||
|
|
ff8fc237e2 | ||
|
|
b99d943b4d | ||
|
|
3199fe08e8 | ||
|
|
7503ce62af | ||
|
|
df8ca250aa | ||
|
|
c71b4bde86 | ||
|
|
3f67802e3d | ||
|
|
093ed23140 | ||
|
|
0f8dee7011 | ||
|
|
0cb557b716 | ||
|
|
8137a776b6 |
24
.gitattributes
vendored
24
.gitattributes
vendored
@@ -1,22 +1,12 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
*text eol=lf
|
||||
* text=auto
|
||||
|
||||
# Explicitly set bash scripts to have unix endings
|
||||
# when checked out on windows
|
||||
*.sh text eol=lf
|
||||
distribution/debian/* text eol=lf
|
||||
macOS/Sonarr text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
*.sln merge=union
|
||||
*.csproj merge=union
|
||||
*.vbproj merge=union
|
||||
*.fsproj merge=union
|
||||
*.dbproj merge=union
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -45,6 +45,10 @@ _dotCover*
|
||||
# DevExpress CodeRush
|
||||
src/.cr/
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
|
||||
# NCrunch
|
||||
*.ncrunch*
|
||||
.*crunch*.local.xml
|
||||
@@ -115,7 +119,9 @@ node_modules/
|
||||
_output*
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_tests*
|
||||
_publish*
|
||||
_temp*
|
||||
*.Result.xml
|
||||
setup/Output/
|
||||
*.~is
|
||||
@@ -133,6 +139,5 @@ output/*
|
||||
.DS_Store
|
||||
|
||||
_start
|
||||
_temp_*/**/*
|
||||
|
||||
src/.idea/
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "src/ExternalModules/CurlSharp"]
|
||||
path = src/ExternalModules/CurlSharp
|
||||
url = https://github.com/Sonarr/CurlSharp.git
|
||||
branch = master
|
||||
@@ -20,7 +20,7 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
|
||||
|
||||
### Requirements
|
||||
|
||||
* [Visual Studio 2017] (https://www.visualstudio.com/vs/)
|
||||
* [Visual Studio 2017](https://www.visualstudio.com/vs/)
|
||||
* [Git](https://git-scm.com/downloads)
|
||||
* [NodeJS](https://nodejs.org/en/download/)
|
||||
* [Yarn](https://yarnpkg.com/)
|
||||
|
||||
185
build.sh
185
build.sh
@@ -1,15 +1,16 @@
|
||||
#! /bin/bash
|
||||
msBuildVersion='15.0'
|
||||
outputFolder='./_output'
|
||||
outputFolderWindows='./_output_windows'
|
||||
outputFolderLinux='./_output_linux'
|
||||
outputFolderMacOS='./_output_macos'
|
||||
outputFolderMacOSApp='./_output_macos_app'
|
||||
testPackageFolder='./_tests/'
|
||||
testSearchPattern='*.Test/bin/x86/Release'
|
||||
testPackageFolder='./_tests'
|
||||
testPackageFolderWindows='./_tests_windows'
|
||||
testPackageFolderLinux='./_tests_linux'
|
||||
sourceFolder='./src'
|
||||
slnFile=$sourceFolder/Sonarr.sln
|
||||
updateFolder=$outputFolder/Sonarr.Update
|
||||
updateFolderMono=$outputFolderLinux/Sonarr.Update
|
||||
updateSubFolder=Sonarr.Update
|
||||
|
||||
nuget='tools/nuget/nuget.exe';
|
||||
vswhere='tools/vswhere/vswhere.exe';
|
||||
@@ -47,7 +48,8 @@ UpdateVersionNumber()
|
||||
verBuild=`echo "${BUILD_NUMBER}" | cut -d. -f4`
|
||||
BUILD_NUMBER=$verMajorMinorRevision.$verBuild
|
||||
echo "##teamcity[buildNumber '$BUILD_NUMBER']"
|
||||
sed -i "s/^[[]assembly: Assembly\(File\|Informational\)\?Version[(]\"[0-9.*]\+\"[)]/[assembly: Assembly\1Version(\"$BUILD_NUMBER\")/g" ./src/NzbDrone*/Properties/AssemblyInfo.cs ./src/Sonarr*/Properties/AssemblyInfo.cs ./src/ServiceHelpers/*/Properties/AssemblyInfo.cs ./src/Common/CommonVersionInfo.cs
|
||||
sed -i "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$BUILD_NUMBER<\/AssemblyVersion>/g" ./src/Directory.Build.props
|
||||
sed -i "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BRANCH:-dev}<\/AssemblyConfiguration>/g" ./src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -86,13 +88,14 @@ CleanFolder()
|
||||
|
||||
BuildWithMSBuild()
|
||||
{
|
||||
installationPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -property installationPath`
|
||||
installationPath=${installationPath/C:\\/\/c\/}
|
||||
installationPath=${installationPath//\\/\/}
|
||||
msBuild="$installationPath/MSBuild/$msBuildVersion/Bin"
|
||||
echo $msBuild
|
||||
msBuildPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -find MSBuild\\\\\*\*\\\\Bin\\\\MSBuild.exe`
|
||||
msBuildPath=${msBuildPath/C:\\/\/c\/}
|
||||
msBuildPath=${msBuildPath//\\/\/}
|
||||
msBuildDir=$(dirname "$msBuildPath")
|
||||
|
||||
export PATH=$msBuild:$PATH
|
||||
echo $msBuildDir
|
||||
|
||||
export PATH=$msBuildDir:$PATH
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Clean //m
|
||||
$nuget restore $slnFile
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
|
||||
@@ -101,15 +104,15 @@ BuildWithMSBuild()
|
||||
BuildWithXbuild()
|
||||
{
|
||||
export MONO_IOMAP=case
|
||||
CheckExitCode xbuild /t:Clean $slnFile
|
||||
CheckExitCode msbuild /t:Clean $slnFile
|
||||
mono $nuget restore $slnFile
|
||||
CheckExitCode xbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||
CheckExitCode msbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||
}
|
||||
|
||||
LintUI()
|
||||
{
|
||||
ProgressStart 'ESLint'
|
||||
CheckExitCode yarn eslint
|
||||
CheckExitCode yarn lint
|
||||
ProgressEnd 'ESLint'
|
||||
|
||||
ProgressStart 'Stylelint'
|
||||
@@ -122,6 +125,7 @@ Build()
|
||||
ProgressStart 'Build'
|
||||
|
||||
rm -rf $outputFolder
|
||||
rm -rf $testPackageFolder
|
||||
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
BuildWithMSBuild
|
||||
@@ -167,6 +171,48 @@ CreateMdbs()
|
||||
fi
|
||||
}
|
||||
|
||||
PatchMono()
|
||||
{
|
||||
local path=$1
|
||||
|
||||
# Below we deal with some mono incompatibilities with windows-only dotnet core/standard libs
|
||||
# See: https://github.com/mono/mono/blob/master/tools/nuget-hash-extractor/download.sh
|
||||
# That list defines assemblies that are prohibited from being loaded from the appdir, instead loading from mono GAC.
|
||||
|
||||
# We have debian dependencies to get these installed or facades from mono 5.10+
|
||||
for assembly in System.IO.Compression System.Runtime.InteropServices.RuntimeInformation System.Net.Http System.Globalization.Extensions System.Text.Encoding.CodePages System.Threading.Overlapped
|
||||
do
|
||||
if [ -e $path/$assembly.dll ]; then
|
||||
if [ -e $sourceFolder/Libraries/Mono/$assembly.dll ]; then
|
||||
echo "Copy Mono-specific facade $assembly.dll (uses win32 interop)"
|
||||
cp $sourceFolder/Libraries/Mono/$assembly.dll $path/$assembly.dll
|
||||
else
|
||||
echo "Remove $assembly.dll (uses win32 interop)"
|
||||
rm $path/$assembly.dll
|
||||
fi
|
||||
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy more stable version of Vectors for mono <5.12
|
||||
if [ -e $path/System.Numerics.Vectors.dll ]; then
|
||||
packageDir="$HOME/.nuget/packages/system.numerics.vectors/4.5.0"
|
||||
|
||||
if [ ! -d "$HOME/.nuget/packages/system.numerics.vectors/4.5.0" ]; then
|
||||
# May reside in the NuGetFallback folder, which is harder to find
|
||||
# Download somewhere to get the real cache populated
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
$nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors
|
||||
else
|
||||
mono $nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors
|
||||
fi
|
||||
rm -rf ./_temp/System.Numerics.Vectors
|
||||
fi
|
||||
# Copy the netstandard2.0 version rather than net46
|
||||
cp "$packageDir/lib/netstandard2.0/System.Numerics.Vectors.dll" $path/
|
||||
fi
|
||||
}
|
||||
|
||||
PackageMono()
|
||||
{
|
||||
ProgressStart 'Creating Mono Package'
|
||||
@@ -190,15 +236,14 @@ PackageMono()
|
||||
rm -f $outputFolderLinux/sqlite3.*
|
||||
rm -f $outputFolderLinux/MediaInfo.*
|
||||
|
||||
PatchMono $outputFolderLinux
|
||||
|
||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $outputFolderLinux
|
||||
|
||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderLinux
|
||||
|
||||
echo "Adding unix System.Runtime.InteropServices.RuntimeInformation.dll (for SharpRaven)"
|
||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux
|
||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux/Sonarr.Update
|
||||
# Remove Http binding redirect by renaming it
|
||||
# We don't need this anymore once our minimum mono version is 5.10
|
||||
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $outputFolderLinux/Sonarr.Console.exe.config
|
||||
|
||||
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
|
||||
rm $outputFolderLinux/Sonarr.exe*
|
||||
@@ -210,7 +255,7 @@ PackageMono()
|
||||
rm $outputFolderLinux/Sonarr.Windows.*
|
||||
|
||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||
cp $outputFolderLinux/Sonarr.Mono.* $updateFolderMono
|
||||
cp $outputFolderLinux/Sonarr.Mono.* $outputFolderLinux/$updateSubFolder/
|
||||
|
||||
ProgressEnd 'Creating Mono Package'
|
||||
}
|
||||
@@ -266,54 +311,92 @@ PackageMacOSApp()
|
||||
ProgressEnd 'Creating macOS App Package'
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
PackageTestsMono()
|
||||
{
|
||||
ProgressStart 'Creating Test Package'
|
||||
ProgressStart 'Creating Mono Test Package'
|
||||
|
||||
rm -rf $testPackageFolder
|
||||
mkdir $testPackageFolder
|
||||
rm -rf $testPackageFolderLinux
|
||||
|
||||
find $sourceFolder -path $testSearchPattern -exec cp -r -u -T "{}" $testPackageFolder \;
|
||||
echo "Copying Binaries"
|
||||
cp -r $testPackageFolder $testPackageFolderLinux
|
||||
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
$nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
|
||||
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
|
||||
else
|
||||
mono $nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
|
||||
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
|
||||
fi
|
||||
|
||||
cp $outputFolder/*.dll $testPackageFolder
|
||||
cp ./test.sh $testPackageFolder
|
||||
echo "Creating MDBs"
|
||||
CreateMdbs $testPackageFolderLinux
|
||||
|
||||
echo "Creating MDBs for tests"
|
||||
CreateMdbs $testPackageFolder
|
||||
echo "Removing PDBs"
|
||||
find $testPackageFolderLinux -name "*.pdb" -exec rm "{}" \;
|
||||
|
||||
rm -f $testPackageFolder/*.log.config
|
||||
|
||||
CleanFolder $testPackageFolder true
|
||||
PatchMono $testPackageFolderLinux
|
||||
|
||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolder
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolderLinux
|
||||
|
||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder
|
||||
# Remove Http binding redirect by renaming it
|
||||
# We don't need this anymore once our minimum mono version is 5.10
|
||||
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $testPackageFolderLinux/Sonarr.Common.Test.dll.config
|
||||
|
||||
echo "Copying CurlSharp libraries"
|
||||
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
|
||||
cp ./test.sh $testPackageFolderLinux/
|
||||
dos2unix $testPackageFolderLinux/test.sh
|
||||
|
||||
ProgressEnd 'Creating Test Package'
|
||||
echo "Removing Sonarr.Windows"
|
||||
rm $testPackageFolderLinux/Sonarr.Windows.*
|
||||
|
||||
rm -f $testPackageFolderLinux/*.log.config
|
||||
|
||||
CleanFolder $testPackageFolderLinux true
|
||||
|
||||
ProgressEnd 'Creating Linux Test Package'
|
||||
}
|
||||
|
||||
CleanupWindowsPackage()
|
||||
PackageTestsWindows()
|
||||
{
|
||||
ProgressStart 'Cleaning Windows Package'
|
||||
ProgressStart 'Creating Windows Test Package'
|
||||
|
||||
rm -rf $testPackageFolderWindows
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $testPackageFolder $testPackageFolderWindows
|
||||
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderWindows
|
||||
else
|
||||
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderWindows
|
||||
fi
|
||||
|
||||
cp ./test.sh $testPackageFolderWindows
|
||||
|
||||
echo "Removing Sonarr.Mono"
|
||||
rm -f $outputFolder/Sonarr.Mono.*
|
||||
rm -f $testPackageFolderWindows/Sonarr.Mono.*
|
||||
|
||||
rm -f $testPackageFolderWindows/*.log.config
|
||||
|
||||
CleanFolder $testPackageFolderWindows true
|
||||
|
||||
ProgressEnd 'Creating Windows Test Package'
|
||||
}
|
||||
|
||||
PackageWindows()
|
||||
{
|
||||
ProgressStart 'Creating Windows Package'
|
||||
|
||||
rm -rf $outputFolderWindows
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $outputFolder $outputFolderWindows
|
||||
|
||||
echo "Removing Sonarr.Mono"
|
||||
rm -f $outputFolderWindows/Sonarr.Mono.*
|
||||
|
||||
echo "Adding Sonarr.Windows to UpdatePackage"
|
||||
cp $outputFolder/Sonarr.Windows.* $updateFolder
|
||||
cp $outputFolderWindows/Sonarr.Windows.* $outputFolderWindows/$updateSubFolder/
|
||||
|
||||
ProgressEnd 'Cleaning Windows Package'
|
||||
ProgressEnd 'Creating Windows Package'
|
||||
}
|
||||
|
||||
PublishArtifacts()
|
||||
@@ -321,10 +404,11 @@ PublishArtifacts()
|
||||
ProgressStart 'Publishing Artifacts'
|
||||
|
||||
# Tests
|
||||
echo "##teamcity[publishArtifacts '_tests/** => tests.zip']"
|
||||
echo "##teamcity[publishArtifacts '$testPackageFolderWindows/** => tests.windows.zip']"
|
||||
echo "##teamcity[publishArtifacts '$testPackageFolderLinux/** => tests.linux.zip']"
|
||||
|
||||
# Releases
|
||||
echo "##teamcity[publishArtifacts '$outputFolder/** => Sonarr.$BRANCH.$BUILD_NUMBER.windows.zip!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderWindows/** => Sonarr.$BRANCH.$BUILD_NUMBER.windows.zip!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderLinux/** => Sonarr.$BRANCH.$BUILD_NUMBER.linux.tar.gz!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderMacOS/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.tar.gz!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderMacOSApp/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.zip']"
|
||||
@@ -354,6 +438,7 @@ RunGulp
|
||||
PackageMono
|
||||
PackageMacOS
|
||||
PackageMacOSApp
|
||||
PackageTests
|
||||
CleanupWindowsPackage
|
||||
PackageTestsMono
|
||||
PackageTestsWindows
|
||||
PackageWindows
|
||||
PublishArtifacts
|
||||
|
||||
7
distribution/build-package.sh
Normal file
7
distribution/build-package.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
# Note, this script is only used for local dev tests, this is not the script used for building the official sonarr package
|
||||
|
||||
mkdir -p /${PWD}/../_output_debian
|
||||
|
||||
docker build -f docker-build/Dockerfile -t sonarr-packager ./docker-build
|
||||
|
||||
docker run --rm -v /${PWD}/../_output_linux:/data/sonarr_bin:ro -v /${PWD}:/data/build -v /${PWD}/../_output_debian:/data/output sonarr-packager
|
||||
@@ -19,7 +19,11 @@ sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/
|
||||
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
||||
|
||||
echo Running debuild for $BuildVersion
|
||||
debuild -b
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
debuild -b
|
||||
else
|
||||
debuild -us -uc -b
|
||||
fi
|
||||
|
||||
# Restore debian directory to the original files
|
||||
rm -rf ./debian
|
||||
@@ -32,16 +36,28 @@ sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debia
|
||||
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
||||
|
||||
echo Running debuild for $BootstrapVersion
|
||||
debuild -b
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
debuild -b
|
||||
else
|
||||
debuild -us -uc -b
|
||||
fi
|
||||
|
||||
echo Moving stuff around
|
||||
mv ../sonarr_*.deb ./
|
||||
mv ../sonarr_*.changes ./
|
||||
rm ../sonarr_*.build
|
||||
|
||||
echo Signing Package
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
echo Signing Package
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
||||
|
||||
echo running alien
|
||||
alien -r -v ./*.deb
|
||||
echo running alien
|
||||
alien -r -v ./*.deb
|
||||
else
|
||||
echo "Exporting packages to ${TEST_OUTPUT}"
|
||||
dpkg -e "sonarr_${BuildVersion}_all.deb" ${TEST_OUTPUT}/sonarr-build
|
||||
dpkg -e "sonarr_${BootstrapVersion}_all.deb" ${TEST_OUTPUT}/sonarr-release
|
||||
|
||||
cp *.deb ${TEST_OUTPUT}/
|
||||
fi
|
||||
|
||||
@@ -7,15 +7,16 @@ Vcs-Git: git@github.com:Sonarr/Sonarr.git
|
||||
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
||||
Build-Depends: debhelper (>= 9),
|
||||
dh-systemd (>= 1.5),
|
||||
mono-devel (>= 4.6),
|
||||
libmono-cil-dev (>= 4.6),
|
||||
cli-common-dev (>= 0.5.7)
|
||||
mono-devel (>= 5.4),
|
||||
libmono-cil-dev (>= 5.4),
|
||||
cli-common-dev (>= 0.9+xamarin5)
|
||||
|
||||
Package: sonarr
|
||||
Architecture: all
|
||||
Provides: nzbdrone
|
||||
Conflicts: nzbdrone
|
||||
Replaces: nzbdrone
|
||||
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52), mono-runtime (>= 5.4), ${cli:Depends}, ${misc:Depends}
|
||||
Recommends: sqlite3 (>= 3.7), mediainfo (>= 0.7.52), ${cli:Recommends}
|
||||
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.4), ca-certificates-mono, libmono-system-net-http4.0-cil (>= 4.0.0~alpha1), ${cli:Depends}, ${misc:Depends}
|
||||
Recommends: libmediainfo0v5 (>= 18.03) | libmediainfo0 (>= 18.03)
|
||||
Suggests: sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
|
||||
Description: Internet PVR
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
sonarr_bin/* /usr/lib/sonarr/bin
|
||||
package_info /usr/lib/sonarr
|
||||
sonarr_bin/* usr/lib/sonarr/bin
|
||||
package_info usr/lib/sonarr
|
||||
|
||||
@@ -104,4 +104,4 @@ fi
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
|
||||
@@ -22,10 +22,25 @@ if [ $1 = "install" ]; then
|
||||
fi
|
||||
|
||||
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
|
||||
# The user used a systemd auto-startup for NzbDrone, we can deal with that.
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
||||
if [ "$psNzbDroneUnit" = "sonarr.service" ]; then
|
||||
# Conflicts with our new sonarr.service so we have to remove it
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and removing..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
if [ -f "/etc/systemd/system/$psNzbDroneUnit" ]; then
|
||||
rm /etc/systemd/system/$psNzbDroneUnit
|
||||
fi
|
||||
if [ -f "/usr/lib/systemd/system/$psNzbDroneUnit" ]; then
|
||||
rm /usr/lib/systemd/system/$psNzbDroneUnit
|
||||
fi
|
||||
deb-systemd-helper purge $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-helper unmask $psNzbDroneUnit >/dev/null
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
else
|
||||
# Just disable it, so the user can revisit the settings later
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
||||
fi
|
||||
else
|
||||
# We don't support auto migration for other startup methods, so bail.
|
||||
# This leaves the sonarr package in an incomplete state.
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
# Sample debian/rules that uses debhelper.
|
||||
# This file was originally written by Joey Hess and Craig Small.
|
||||
# As a special exception, when this file is copied by dh-make into a
|
||||
# dh-make output file, you may use that output file without restriction.
|
||||
# This special exception was added by Craig Small in version 0.37 of dh-make.
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
# Note: System.Native is a dependency of System.Runtime.InteropServices.RuntimeInformation used by SharpRaven,
|
||||
# but SharpRaven doesn't use any functions that need System.Native
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi System.Native
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd --with=cli
|
||||
@@ -20,7 +12,7 @@ EXCLUDE_MODULEREFS = crypt32 httpapi System.Native
|
||||
override_dh_installinit:
|
||||
true
|
||||
|
||||
# Sonarr like debug symbols for logging
|
||||
# Sonarr likes debug symbols for logging
|
||||
override_dh_clistrip:
|
||||
|
||||
override_dh_makeclilibs:
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
recommends libcurl3
|
||||
ignores msbuild
|
||||
ignores msbuild
|
||||
ignores libmediainfo0v5
|
||||
|
||||
23
distribution/docker-build/Dockerfile
Normal file
23
distribution/docker-build/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM ubuntu:xenial AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV MONO_VERSION 5.14
|
||||
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||
apt-get update && apt-get install -y \
|
||||
devscripts build-essential tofrodos \
|
||||
dh-make dh-systemd \
|
||||
cli-common-dev \
|
||||
mono-complete \
|
||||
sqlite3 libcurl3 mediainfo
|
||||
|
||||
RUN apt-cache policy mono-complete
|
||||
RUN apt-cache policy cli-common-dev
|
||||
|
||||
COPY debian-start.sh /debian-start.sh
|
||||
RUN fromdos /debian-start.sh
|
||||
|
||||
WORKDIR /data
|
||||
VOLUME [ "/data/sonarr_bin", "/data/build", "/data/output" ]
|
||||
CMD /debian-start.sh
|
||||
18
distribution/docker-build/debian-start.sh
Normal file
18
distribution/docker-build/debian-start.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
echo "Debian Build Dev bootstrap..."
|
||||
|
||||
export TEST_OUTPUT=/data/output
|
||||
|
||||
mkdir ${TEST_OUTPUT}
|
||||
|
||||
mkdir /data/temp
|
||||
|
||||
cp -rf /data/build/debian.sh /data/temp
|
||||
cp -rf /data/build/debian /data/temp
|
||||
cp -rf /data/sonarr_bin /data/temp/sonarr_bin
|
||||
|
||||
cd /data/temp
|
||||
|
||||
ls -al .
|
||||
|
||||
fromdos debian.sh
|
||||
sh debian.sh
|
||||
22
docker/tests/mono/complete/Dockerfile
Normal file
22
docker/tests/mono/complete/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ARG MONO_VERSION=5.20
|
||||
ARG MONO_URL=stable-xenial/snapshots/$MONO_VERSION
|
||||
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||
echo "deb http://download.mono-project.com/repo/debian $MONO_URL main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||
apt-get update && apt-get install -y \
|
||||
tofrodos tzdata \
|
||||
mono-complete \
|
||||
sqlite3 mediainfo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY startup.sh /startup.sh
|
||||
RUN fromdos /startup.sh
|
||||
|
||||
WORKDIR /data/
|
||||
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||
|
||||
CMD bash /startup.sh
|
||||
|
||||
28
docker/tests/mono/sonarr/Dockerfile
Normal file
28
docker/tests/mono/sonarr/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ARG MONO_VERSION=5.20
|
||||
ARG MONO_URL=stable-xenial/snapshots/$MONO_VERSION
|
||||
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||
echo "deb http://download.mono-project.com/repo/debian $MONO_URL main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 2009837CBFFD68F45BC180471F4F90DE2A9B4BF8 && \
|
||||
echo "deb http://apt.sonarr.tv/ubuntu xenial main" > /etc/apt/sources.list.d/sonarr.list && \
|
||||
apt-get update && apt-get install -y \
|
||||
tofrodos tzdata \
|
||||
sonarr \
|
||||
sqlite3 mediainfo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libmono-system-runtime4.0-cil \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY startup.sh /startup.sh
|
||||
RUN fromdos /startup.sh
|
||||
|
||||
WORKDIR /data/
|
||||
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||
|
||||
CMD bash /startup.sh
|
||||
|
||||
15
docker/tests/mono/startup.sh
Normal file
15
docker/tests/mono/startup.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
echo "Preparing Test..."
|
||||
mkdir -p /data/test
|
||||
cp -r /data/_tests_linux/* /data/test/
|
||||
cp -r /data/_output_linux /data/test/bin
|
||||
|
||||
cd /data/test
|
||||
|
||||
runTest()
|
||||
{
|
||||
bash test.sh Linux $1
|
||||
cp TestResult.xml /data/_tests_results/TestResult_$1.xml
|
||||
}
|
||||
|
||||
runTest Integration
|
||||
runTest Unit
|
||||
108
docker/tests/run-all.sh
Normal file
108
docker/tests/run-all.sh
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
opt_parallel=
|
||||
opt_version=
|
||||
opt_mode=both
|
||||
while getopts 'pv:m:?h' c
|
||||
do
|
||||
case $c in
|
||||
p) opt_parallel=1 ;;
|
||||
v) opt_version=$OPTARG ;;
|
||||
m) opt_mode=$OPTARG ;;
|
||||
?|h) printf "Usage: %s [-p] [-v mono-ver] [-m sonarr|complete]\n" $0
|
||||
printf " -p run parallel\n"
|
||||
printf " -v run specified mono version\n"
|
||||
printf " -m run only mono-'complete' or 'sonarr' package variants\n"
|
||||
exit 2
|
||||
esac
|
||||
done
|
||||
# NOTE:
|
||||
# each container has a 1gb tmpfs mounted since it greatly speeds up the normally intensive db operations
|
||||
# make sure that the docker host has enough memory to handle about ~300 MB per container, so 2-3 GB total
|
||||
# excess goes to the swap and will slow down the entire system
|
||||
|
||||
# Preferred versions
|
||||
MONO_VERSIONS="6.8 6.6 6.4 6.0 5.20 5.18"
|
||||
|
||||
# Future versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 6.10=preview-xenial"
|
||||
|
||||
# Supported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12 5.10 5.8 5.4"
|
||||
|
||||
# Legacy unsupported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.0"
|
||||
#MONO_VERSIONS="$MONO_VERSIONS 4.8=stable-wheezy/snapshots/4.8"
|
||||
|
||||
if [ "$opt_version" != "" ]; then
|
||||
MONO_VERSIONS="$opt_version"
|
||||
fi
|
||||
|
||||
mkdir -p ${PWD}/../../_tests_results
|
||||
|
||||
prepOne() {
|
||||
local MONO_VERSION_PAIR=$1
|
||||
|
||||
MONO_VERSION_SPLIT=(${MONO_VERSION_PAIR//=/ })
|
||||
MONO_VERSION=${MONO_VERSION_SPLIT[0]}
|
||||
MONO_URL=${MONO_VERSION_SPLIT[1]:-"stable-xenial/snapshots/$MONO_VERSION"}
|
||||
|
||||
echo "Building Test Docker for mono $MONO_VERSION"
|
||||
|
||||
if [ "$opt_mode" != "sonarr" ]; then
|
||||
docker build -t sonarr-test-$MONO_VERSION --build-arg MONO_VERSION=$MONO_VERSION --build-arg MONO_URL=$MONO_URL --file mono/complete/Dockerfile mono
|
||||
fi
|
||||
|
||||
if [ "$opt_mode" != "complete" ] && [ "$MONO_VERSION" != "5.0" ]; then
|
||||
docker build -t sonarr-test-$MONO_VERSION-sonarr --build-arg MONO_VERSION=$MONO_VERSION --build-arg MONO_URL=$MONO_URL --file mono/sonarr/Dockerfile mono
|
||||
fi
|
||||
}
|
||||
|
||||
runOne() {
|
||||
local MONO_VERSION_PAIR=$1
|
||||
|
||||
MONO_VERSION_SPLIT=(${MONO_VERSION_PAIR//=/ })
|
||||
MONO_VERSION=${MONO_VERSION_SPLIT[0]}
|
||||
|
||||
echo "Running Test Docker for mono $MONO_VERSION"
|
||||
|
||||
if [ "$opt_mode" != "sonarr" ]; then
|
||||
dockerArgs="--rm"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_linux:/data/_tests_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_output_linux:/data/_output_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_results/mono-$MONO_VERSION:/data/_tests_results"
|
||||
dockerArgs="$dockerArgs --mount type=tmpfs,destination=//data/test,tmpfs-size=1g"
|
||||
docker run $dockerArgs sonarr-test-$MONO_VERSION
|
||||
fi
|
||||
|
||||
if [ "$opt_mode" != "complete" ] && [ "$MONO_VERSION" != "5.0" ]; then
|
||||
dockerArgs="--rm"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_linux:/data/_tests_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_output_linux:/data/_output_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_results/mono-$MONO_VERSION-sonarr:/data/_tests_results"
|
||||
dockerArgs="$dockerArgs --mount type=tmpfs,destination=//data/test,tmpfs-size=1g"
|
||||
docker run $dockerArgs sonarr-test-$MONO_VERSION-sonarr
|
||||
fi
|
||||
|
||||
echo "Finished Test Docker for mono $MONO_VERSION"
|
||||
}
|
||||
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||
prepOne "$MONO_VERSION_PAIR"
|
||||
done
|
||||
fi
|
||||
|
||||
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
runOne "$MONO_VERSION_PAIR" &
|
||||
else
|
||||
prepOne "$MONO_VERSION_PAIR"
|
||||
runOne "$MONO_VERSION_PAIR"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
echo "Waiting for all runs to finish"
|
||||
wait
|
||||
echo "Finished all runs"
|
||||
fi
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = [
|
||||
'>0.25%',
|
||||
'not ie 11',
|
||||
'not op_mini all',
|
||||
'not chrome < 60'
|
||||
];
|
||||
@@ -5,14 +5,17 @@ const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const errorHandler = require('./helpers/errorHandler');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const browsers = require('../browsers');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
const uiFolder = 'UI';
|
||||
const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = process.argv.indexOf('--production') > -1;
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
console.log('Source Folder:', srcFolder);
|
||||
console.log('Output Folder:', distFolder);
|
||||
console.log('isProduction:', isProduction);
|
||||
|
||||
const cssVarsFiles = [
|
||||
@@ -23,6 +26,22 @@ const cssVarsFiles = [
|
||||
'../src/Styles/Variables/zIndexes'
|
||||
].map(require.resolve);
|
||||
|
||||
// Override the way HtmlWebpackPlugin injects the scripts
|
||||
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
||||
const head = assetTags.head.map((v) => {
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
|
||||
return this.createHtmlTag(v);
|
||||
});
|
||||
const body = assetTags.body.map((v) => {
|
||||
v.attributes = { src: `/${v.attributes.src}` };
|
||||
return this.createHtmlTag(v);
|
||||
});
|
||||
|
||||
return html
|
||||
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
|
||||
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
@@ -30,7 +49,12 @@ const plugins = [
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: path.join('_output', uiFolder, 'Content', 'styles.css')
|
||||
filename: path.join('Content', 'styles.css')
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.html',
|
||||
filename: 'index.html'
|
||||
})
|
||||
];
|
||||
|
||||
@@ -47,8 +71,6 @@ const config = {
|
||||
},
|
||||
|
||||
entry: {
|
||||
preload: 'preload.js',
|
||||
vendor: 'vendor.js',
|
||||
index: 'index.js'
|
||||
},
|
||||
|
||||
@@ -64,12 +86,20 @@ const config = {
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: path.join('_output', uiFolder, '[name].js'),
|
||||
path: distFolder,
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
chunkIds: 'named'
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial'
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
plugins,
|
||||
@@ -100,7 +130,7 @@ const config = {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
targets: browsers
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -119,8 +149,9 @@ const config = {
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]',
|
||||
modules: true
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -184,7 +215,7 @@ const config = {
|
||||
|
||||
gulp.task('webpack', () => {
|
||||
return webpackStream(config)
|
||||
.pipe(gulp.dest('./'));
|
||||
.pipe(gulp.dest('_output/UI'));
|
||||
});
|
||||
|
||||
gulp.task('webpackWatch', () => {
|
||||
@@ -192,7 +223,7 @@ gulp.task('webpackWatch', () => {
|
||||
|
||||
return webpackStream(config)
|
||||
.on('error', errorHandler)
|
||||
.pipe(gulp.dest('./'))
|
||||
.pipe(gulp.dest('_output/UI'))
|
||||
.on('error', errorHandler)
|
||||
.pipe(livereload())
|
||||
.on('error', errorHandler);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const reload = require('require-nocache')(module);
|
||||
const browsers = require('./browsers');
|
||||
|
||||
module.exports = (ctx, configPath, options) => {
|
||||
const config = {
|
||||
@@ -16,10 +15,7 @@ module.exports = (ctx, configPath, options) => {
|
||||
}, {})
|
||||
},
|
||||
'postcss-color-function': {},
|
||||
'postcss-nested': {},
|
||||
autoprefixer: {
|
||||
browsers
|
||||
}
|
||||
'postcss-nested': {}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -232,6 +232,30 @@ function HistoryDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadIgnored') {
|
||||
const {
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
|
||||
@@ -23,6 +23,8 @@ function getHeaderTitle(eventType) {
|
||||
return 'Episode File Deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode File Renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Download Ignored';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ function getIconName(eventType) {
|
||||
return icons.DELETE;
|
||||
case 'episodeFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
return icons.UNKNOWN;
|
||||
}
|
||||
@@ -47,6 +49,8 @@ function getTooltip(eventType, data) {
|
||||
return 'Episode file deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode file renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Episode Download Ignored';
|
||||
default:
|
||||
return 'Unknown event';
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class Queue extends Component {
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
@@ -107,8 +107,8 @@ class Queue extends Component {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = (blacklist) => {
|
||||
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class Queue extends Component {
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -145,10 +145,11 @@ class Queue extends Component {
|
||||
isPendingSelected
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
|
||||
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
||||
const hasError = error || episodesError;
|
||||
const selectedCount = this.getSelectedIds().length;
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
@@ -259,6 +260,13 @@ class Queue extends Component {
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.seriesId && item.episodeId);
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
@@ -279,7 +287,7 @@ Queue.propTypes = {
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -18,13 +18,13 @@ function createMapStateToProps() {
|
||||
(state) => state.episodes,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
||||
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => {
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
isEpisodesFetching: episodes.isFetching,
|
||||
isEpisodesPopulated: episodes.isPopulated,
|
||||
episodesError: episodes.error,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
@@ -129,7 +129,7 @@ class QueueConnector extends Component {
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@ class QueueConnector extends Component {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = (ids, blacklist) => {
|
||||
this.props.removeQueueItems({ ids, blacklist });
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -68,6 +68,7 @@ class QueueRow extends Component {
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
series,
|
||||
@@ -100,8 +101,8 @@ class QueueRow extends Component {
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
|
||||
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
|
||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
@@ -129,6 +130,7 @@ class QueueRow extends Component {
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
@@ -350,6 +352,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!(series && episode)}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
@@ -365,6 +368,7 @@ QueueRow.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadState: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
series: PropTypes.object,
|
||||
|
||||
@@ -43,8 +43,8 @@ class QueueRowConnector extends Component {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
|
||||
onRemoveQueueItemPress = (blacklist) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, blacklist });
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -37,13 +37,14 @@ function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus = 'Ok',
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'Warning';
|
||||
const hasError = trackedDownloadStatus === 'Error';
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
@@ -54,46 +55,58 @@ function QueueStatusCell(props) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'Paused') {
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
}
|
||||
|
||||
if (status === 'Queued') {
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
}
|
||||
|
||||
if (status === 'Completed') {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = 'Downloaded';
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ' - Waiting to Import';
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ' - Importing';
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ' - Waiting to Process';
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'Delay') {
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = 'Pending - Download client is unavailable';
|
||||
}
|
||||
|
||||
if (status === 'Failed') {
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
}
|
||||
|
||||
if (status === 'Warning') {
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'Completed') {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
@@ -125,9 +138,15 @@ function QueueStatusCell(props) {
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: 'Ok',
|
||||
trackedDownloadState: 'Downloading'
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
@@ -21,26 +20,41 @@ class RemoveQueueItemModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@@ -50,10 +64,11 @@ class RemoveQueueItemModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -69,17 +84,31 @@ class RemoveQueueItemModal extends Component {
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
<div>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -93,7 +122,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -107,6 +136,7 @@ class RemoveQueueItemModal extends Component {
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -21,26 +21,41 @@ class RemoveQueueItemsModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@@ -50,10 +65,11 @@ class RemoveQueueItemsModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount
|
||||
selectedCount,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -74,7 +90,23 @@ class RemoveQueueItemsModal extends Component {
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
@@ -93,7 +125,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -107,6 +139,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -35,14 +35,20 @@
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.noSeriesText {
|
||||
margin-top: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -78,7 +79,8 @@ class AddNewSeries extends Component {
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
items
|
||||
items,
|
||||
hasExistingSeries
|
||||
} = this.props;
|
||||
|
||||
const term = this.state.term;
|
||||
@@ -121,8 +123,13 @@ class AddNewSeries extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Failed to load search results, please try again.</div>
|
||||
!isFetching && !!error ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Failed to load search results, please try again.
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -155,13 +162,34 @@ class AddNewSeries extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!term &&
|
||||
term ?
|
||||
null :
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>It's easy to add a new series, just start typing the name the series you want to add.</div>
|
||||
<div className={styles.helpText}>
|
||||
It's easy to add a new series, just start typing the name the series you want to add.
|
||||
</div>
|
||||
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!term && !hasExistingSeries ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noSeriesText}>
|
||||
You haven't added any series yet, do you want to import some or all of your series first?
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
to="/add/import"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
Import Existing Series
|
||||
</Button>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div />
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
@@ -176,6 +204,7 @@ AddNewSeries.propTypes = {
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasExistingSeries: PropTypes.bool.isRequired,
|
||||
onSeriesLookupChange: PropTypes.func.isRequired,
|
||||
onClearSeriesLookup: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -10,13 +10,15 @@ import AddNewSeries from './AddNewSeries';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addSeries,
|
||||
(state) => state.series.items.length,
|
||||
(state) => state.router.location,
|
||||
(addSeries, location) => {
|
||||
(addSeries, existingSeriesCount, location) => {
|
||||
const { params } = parseUrl(location.search);
|
||||
|
||||
return {
|
||||
...addSeries,
|
||||
term: params.term,
|
||||
...addSeries
|
||||
hasExistingSeries: existingSeriesCount > 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||
import styles from './AddNewSeriesModalContent.css';
|
||||
@@ -27,10 +28,19 @@ class AddNewSeriesModalContent extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
|
||||
props.seriesType.value :
|
||||
props.initialSeriesType,
|
||||
searchForMissingEpisodes: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.seriesType.value !== prevProps.seriesType.value) {
|
||||
this.setState({ seriesType: this.props.seriesType.value });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -47,7 +57,12 @@ class AddNewSeriesModalContent extends Component {
|
||||
}
|
||||
|
||||
onAddSeriesPress = () => {
|
||||
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
|
||||
const {
|
||||
searchForMissingEpisodes,
|
||||
seriesType
|
||||
} = this.state;
|
||||
|
||||
this.props.onAddSeriesPress(searchForMissingEpisodes, seriesType);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -66,9 +81,11 @@ class AddNewSeriesModalContent extends Component {
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
folder,
|
||||
tags,
|
||||
showLanguageProfile,
|
||||
isSmallScreen,
|
||||
isWindows,
|
||||
onModalClose,
|
||||
onInputChange,
|
||||
...otherProps
|
||||
@@ -115,6 +132,15 @@ class AddNewSeriesModalContent extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
valueOptions={{
|
||||
seriesFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
selectedValueOptions={{
|
||||
seriesFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
helpText={`'${folder}' subfolder will be created automatically`}
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
@@ -189,6 +215,7 @@ class AddNewSeriesModalContent extends Component {
|
||||
name="seriesType"
|
||||
onChange={onInputChange}
|
||||
{...seriesType}
|
||||
value={this.state.seriesType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -251,6 +278,7 @@ AddNewSeriesModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
overview: PropTypes.string,
|
||||
initialSeriesType: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
@@ -260,9 +288,11 @@ AddNewSeriesModalContent.propTypes = {
|
||||
languageProfileId: PropTypes.object,
|
||||
seriesType: PropTypes.object.isRequired,
|
||||
seasonFolder: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onAddSeriesPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAddSeriesDefault, addSeries } from 'Store/Actions/addSeriesActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewSeriesModalContent from './AddNewSeriesModalContent';
|
||||
|
||||
@@ -12,7 +13,8 @@ function createMapStateToProps() {
|
||||
(state) => state.addSeries,
|
||||
(state) => state.settings.languageProfiles,
|
||||
createDimensionsSelector(),
|
||||
(addSeriesState, languageProfiles, dimensions) => {
|
||||
createSystemStatusSelector(),
|
||||
(addSeriesState, languageProfiles, dimensions, systemStatus) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
@@ -32,6 +34,7 @@ function createMapStateToProps() {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
isWindows: systemStatus.isWindows,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
@@ -52,14 +55,13 @@ class AddNewSeriesModalContentConnector extends Component {
|
||||
this.props.setAddSeriesDefault({ [name]: value });
|
||||
}
|
||||
|
||||
onAddSeriesPress = (searchForMissingEpisodes) => {
|
||||
onAddSeriesPress = (searchForMissingEpisodes, seriesType) => {
|
||||
const {
|
||||
tvdbId,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
tags
|
||||
} = this.props;
|
||||
@@ -70,7 +72,7 @@ class AddNewSeriesModalContentConnector extends Component {
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
languageProfileId: languageProfileId.value,
|
||||
seriesType: seriesType.value,
|
||||
seriesType,
|
||||
seasonFolder: seasonFolder.value,
|
||||
tags: tags.value,
|
||||
searchForMissingEpisodes
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
.searchResult {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
@add-mixin cover;
|
||||
|
||||
background-color: $white;
|
||||
transition: background 500ms;
|
||||
|
||||
&:hover {
|
||||
@@ -14,13 +19,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@add-mixin linkOverlay;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 0 1 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
@@ -30,6 +47,18 @@
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.tvdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-top: -4px;
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.tvdbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
|
||||
@@ -24,7 +24,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
|
||||
this.onAddSerisModalClose();
|
||||
this.onAddSeriesModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,14 @@ class AddNewSeriesSearchResult extends Component {
|
||||
this.setState({ isNewAddSeriesModalOpen: true });
|
||||
}
|
||||
|
||||
onAddSerisModalClose = () => {
|
||||
onAddSeriesModalClose = () => {
|
||||
this.setState({ isNewAddSeriesModalOpen: false });
|
||||
}
|
||||
|
||||
onTVDBLinkPress = (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -53,6 +57,8 @@ class AddNewSeriesSearchResult extends Component {
|
||||
overview,
|
||||
statistics,
|
||||
ratings,
|
||||
folder,
|
||||
seriesType,
|
||||
images,
|
||||
isExistingSeries,
|
||||
isSmallScreen
|
||||
@@ -72,11 +78,13 @@ class AddNewSeriesSearchResult extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.searchResult}>
|
||||
<Link
|
||||
className={styles.searchResult}
|
||||
className={styles.underlay}
|
||||
{...linkProps}
|
||||
>
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
{
|
||||
isSmallScreen ?
|
||||
null :
|
||||
@@ -88,7 +96,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
|
||||
@@ -110,6 +118,18 @@ class AddNewSeriesSearchResult extends Component {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.tvdbLink}
|
||||
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||
onPress={this.onTVDBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tvdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -146,13 +166,24 @@ class AddNewSeriesSearchResult extends Component {
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
status === 'upcoming' ?
|
||||
<Label
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Upcoming
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<AddNewSeriesModal
|
||||
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
|
||||
@@ -160,8 +191,10 @@ class AddNewSeriesSearchResult extends Component {
|
||||
title={title}
|
||||
year={year}
|
||||
overview={overview}
|
||||
folder={folder}
|
||||
initialSeriesType={seriesType}
|
||||
images={images}
|
||||
onModalClose={this.onAddSerisModalClose}
|
||||
onModalClose={this.onAddSeriesModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -178,6 +211,8 @@ AddNewSeriesSearchResult.propTypes = {
|
||||
overview: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingSeries: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
|
||||
@@ -90,6 +90,7 @@ function ImportSeriesRow(props) {
|
||||
<ImportSeriesSelectSeriesConnector
|
||||
id={id}
|
||||
isExistingSeries={isExistingSeries}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
</VirtualTableRow>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -41,13 +41,23 @@ class ImportSeriesSelectSeriesConnector extends Component {
|
||||
onSeriesSelect = (tvdbId) => {
|
||||
const {
|
||||
id,
|
||||
items
|
||||
items,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
|
||||
const selectedSeries = items.find((item) => item.tvdbId === tvdbId);
|
||||
|
||||
this.props.setImportSeriesValue({
|
||||
id,
|
||||
selectedSeries: _.find(items, { tvdbId })
|
||||
selectedSeries
|
||||
});
|
||||
|
||||
if (selectedSeries.seriesType !== seriesTypes.STANDARD) {
|
||||
onInputChange({
|
||||
name: 'seriesType',
|
||||
value: selectedSeries.seriesType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -69,6 +79,7 @@ ImportSeriesSelectSeriesConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedSeries: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
queueLookupSeries: PropTypes.func.isRequired,
|
||||
setImportSeriesValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -75,10 +75,10 @@ class ImportSeriesSelectFolder extends Component {
|
||||
Some tips to ensure the import goes smoothly:
|
||||
<ul>
|
||||
<li className={styles.tip}>
|
||||
Make sure your files include the quality in the name. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
|
||||
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
|
||||
</li>
|
||||
<li className={styles.tip}>
|
||||
Point Sonarr to the folder containing all of your tv shows not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
|
||||
Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
flex: 1 0 14.28%;
|
||||
overflow: hidden;
|
||||
min-height: 70px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
border-left: 1px solid $borderColor;
|
||||
border-bottom: 1px solid $calendarBorderColor;
|
||||
border-left: 1px solid $calendarBorderColor;
|
||||
}
|
||||
|
||||
.isSingleDay {
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
.dayOfMonth {
|
||||
padding-right: 5px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
border-bottom: 1px solid $calendarBorderColor;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.days {
|
||||
display: flex;
|
||||
border-right: 1px solid $borderColor;
|
||||
border-right: 1px solid $calendarBorderColor;
|
||||
}
|
||||
|
||||
.day,
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.episodeInfo {
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.seriesTitle,
|
||||
.episodeTitle {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex: 1 0 1px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -37,6 +40,10 @@
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.airTime {
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
|
||||
@@ -200,7 +200,7 @@ class CalendarEvent extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<div className={styles.airTime}>
|
||||
{formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
.seriesTitle {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex: 1 0 1px;
|
||||
margin-right: 10px;
|
||||
color: #3a3f51;
|
||||
@@ -23,10 +22,12 @@
|
||||
|
||||
.airTime {
|
||||
flex: 1 0 1px;
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.episodeInfo {
|
||||
margin-left: 10px;
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.absoluteEpisodeNumber {
|
||||
@@ -80,3 +81,7 @@
|
||||
.premiere {
|
||||
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.unaired {
|
||||
composes: unaired from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const APPLICATION_UPDATE = 'ApplicationUpdate';
|
||||
export const BACKUP = 'Backup';
|
||||
export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload';
|
||||
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
|
||||
export const CLEAR_BLACKLIST = 'ClearBlacklist';
|
||||
export const CLEAR_LOGS = 'ClearLog';
|
||||
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
||||
|
||||
@@ -56,7 +56,7 @@ class ErrorBoundary extends Component {
|
||||
|
||||
ErrorBoundary.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
errorComponent: PropTypes.func.isRequired
|
||||
errorComponent: PropTypes.elementType.isRequired
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
||||
@@ -144,6 +144,7 @@ class FileBrowserModalContent extends Component {
|
||||
<Scroller
|
||||
ref={this.setScrollerRef}
|
||||
className={styles.scroller}
|
||||
scrollDirection={scrollDirections.BOTH}
|
||||
>
|
||||
{
|
||||
!!error &&
|
||||
@@ -152,7 +153,10 @@ class FileBrowserModalContent extends Component {
|
||||
|
||||
{
|
||||
isPopulated && !error &&
|
||||
<Table columns={columns}>
|
||||
<Table
|
||||
horizontalScroll={false}
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
emptyParent &&
|
||||
|
||||
@@ -132,6 +132,7 @@ class FilterBuilderModalContent extends Component {
|
||||
filterBuilderProps,
|
||||
isSaving,
|
||||
saveError,
|
||||
onCancelPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
@@ -190,7 +191,7 @@ class FilterBuilderModalContent extends Component {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
<Button onPress={onCancelPress}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -220,6 +221,7 @@ FilterBuilderModalContent.propTypes = {
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -3,16 +3,21 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
return createSelector(
|
||||
(state, { filterType }) => filterType,
|
||||
(state, { sectionItems }) => sectionItems,
|
||||
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
|
||||
(sectionItems, selectedFilterBuilderProp) => {
|
||||
(filterType, sectionItems, selectedFilterBuilderProp) => {
|
||||
if (
|
||||
selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING
|
||||
(selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
|
||||
filterType !== filterTypes.EQUAL &&
|
||||
filterType !== filterBuilderTypes.NOT_EQUAL ||
|
||||
!selectedFilterBuilderProp.optionsSelector
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.tag {
|
||||
height: 21px;
|
||||
|
||||
&.isLastTag {
|
||||
.or {
|
||||
display: none;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
const seriesStatusList = [
|
||||
{ id: 'continuing', name: 'Continuing' },
|
||||
{ id: 'upcoming', name: 'Upcoming' },
|
||||
{ id: 'ended', name: 'Ended' }
|
||||
];
|
||||
|
||||
function SeriesStatusFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
tagList={seriesStatusList}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -29,10 +29,10 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters.map((customFilter, index) => {
|
||||
customFilters.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={index}
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
|
||||
@@ -34,6 +34,17 @@ class FilterModal extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onCancelPress = () => {
|
||||
if (this.state.filterBuilder) {
|
||||
this.setState({
|
||||
filterBuilder: false,
|
||||
id: null
|
||||
});
|
||||
} else {
|
||||
this.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({
|
||||
filterBuilder: false,
|
||||
@@ -67,6 +78,7 @@ class FilterModal extends Component {
|
||||
<FilterBuilderModalContentConnector
|
||||
{...otherProps}
|
||||
id={id}
|
||||
onCancelPress={this.onCancelPress}
|
||||
onModalClose={this.onModalClose}
|
||||
/> :
|
||||
<CustomFiltersModalContentConnector
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
.suggestionsList {
|
||||
margin: 5px 0;
|
||||
padding-left: 0;
|
||||
max-height: 200px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ class AutoSuggestInput extends Component {
|
||||
className: classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
hasWarning && styles.hasWarning
|
||||
),
|
||||
name,
|
||||
value,
|
||||
@@ -234,7 +234,7 @@ AutoSuggestInput.propTypes = {
|
||||
minHeight: PropTypes.number.isRequired,
|
||||
maxHeight: PropTypes.number.isRequired,
|
||||
getSuggestionValue: PropTypes.func.isRequired,
|
||||
renderInputComponent: PropTypes.func,
|
||||
renderInputComponent: PropTypes.elementType,
|
||||
renderSuggestion: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func,
|
||||
onInputKeyDown: PropTypes.func,
|
||||
|
||||
@@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDevices, clearDevices } from 'Store/Actions/deviceActions';
|
||||
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
|
||||
import DeviceInput from './DeviceInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.devices,
|
||||
(state) => state.providerOptions,
|
||||
(value, devices) => {
|
||||
|
||||
return {
|
||||
@@ -37,8 +37,8 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchDevices: fetchDevices,
|
||||
dispatchClearDevices: clearDevices
|
||||
dispatchFetchOptions: fetchOptions,
|
||||
dispatchClearOptions: clearOptions
|
||||
};
|
||||
|
||||
class DeviceInputConnector extends Component {
|
||||
@@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
// this.props.dispatchClearDevices();
|
||||
this.props.dispatchClearOptions();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -61,10 +61,14 @@ class DeviceInputConnector extends Component {
|
||||
const {
|
||||
provider,
|
||||
providerData,
|
||||
dispatchFetchDevices
|
||||
dispatchFetchOptions
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchDevices({ provider, providerData });
|
||||
dispatchFetchOptions({
|
||||
action: 'getDevices',
|
||||
provider,
|
||||
providerData
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
@@ -92,8 +96,8 @@ DeviceInputConnector.propTypes = {
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchDevices: PropTypes.func.isRequired,
|
||||
dispatchClearDevices: PropTypes.func.isRequired
|
||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||
dispatchClearOptions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import isMobileUtil from 'Utilities/isMobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -262,6 +262,7 @@ class EnhancedSelectInput extends Component {
|
||||
isDisabled,
|
||||
hasError,
|
||||
hasWarning,
|
||||
valueOptions,
|
||||
selectedValueOptions,
|
||||
selectedValueComponent: SelectedValueComponent,
|
||||
optionComponent: OptionComponent
|
||||
@@ -363,6 +364,7 @@ class EnhancedSelectInput extends Component {
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
isSelected={index === selectedIndex}
|
||||
{...valueOptions}
|
||||
{...v}
|
||||
isMobile={false}
|
||||
onSelect={this.onSelect}
|
||||
@@ -404,6 +406,7 @@ class EnhancedSelectInput extends Component {
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
isSelected={index === selectedIndex}
|
||||
{...valueOptions}
|
||||
{...v}
|
||||
isMobile={true}
|
||||
onSelect={this.onSelect}
|
||||
@@ -431,9 +434,10 @@ EnhancedSelectInput.propTypes = {
|
||||
isDisabled: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
valueOptions: PropTypes.object.isRequired,
|
||||
selectedValueOptions: PropTypes.object.isRequired,
|
||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
optionComponent: PropTypes.func,
|
||||
optionComponent: PropTypes.elementType,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -441,6 +445,7 @@ EnhancedSelectInput.defaultProps = {
|
||||
className: styles.enhancedSelect,
|
||||
disabledClassName: styles.isDisabled,
|
||||
isDisabled: false,
|
||||
valueOptions: {},
|
||||
selectedValueOptions: {},
|
||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||
optionComponent: HintedSelectInputOption
|
||||
|
||||
@@ -63,7 +63,7 @@ class EnhancedSelectInputOption extends Component {
|
||||
|
||||
EnhancedSelectInputOption.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -14,6 +14,7 @@ import PasswordInput from './PasswordInput';
|
||||
import PathInputConnector from './PathInputConnector';
|
||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
@@ -61,6 +62,9 @@ function getComponent(type) {
|
||||
case inputTypes.LANGUAGE_PROFILE_SELECT:
|
||||
return LanguageProfileSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.ROOT_FOLDER_SELECT:
|
||||
return RootFolderSelectInputConnector;
|
||||
|
||||
|
||||
106
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
106
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexers,
|
||||
(state, { includeAny }) => includeAny,
|
||||
(indexers, includeAny) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||
return {
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexers
|
||||
};
|
||||
|
||||
class IndexerSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
values
|
||||
} = this.props;
|
||||
|
||||
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) {
|
||||
this.onChange({ name, value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerSelectInputConnector.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeAny: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
IndexerSelectInputConnector.defaultProps = {
|
||||
includeAny: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
||||
@@ -18,10 +18,19 @@ class PathInput extends Component {
|
||||
this._node = document.getElementById('portal-root');
|
||||
|
||||
this.state = {
|
||||
value: props.value,
|
||||
isFileBrowserModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { value } = this.props;
|
||||
|
||||
if (prevProps.value !== value) {
|
||||
this.setState({ value });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
@@ -51,11 +60,8 @@ class PathInput extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = (event, { newValue }) => {
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
});
|
||||
onInputChange = ({ value }) => {
|
||||
this.setState({ value });
|
||||
}
|
||||
|
||||
onInputKeyDown = (event) => {
|
||||
@@ -77,6 +83,11 @@ class PathInput extends Component {
|
||||
}
|
||||
|
||||
onInputBlur = () => {
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: this.state.value
|
||||
});
|
||||
|
||||
this.props.onClearPaths();
|
||||
}
|
||||
|
||||
@@ -108,13 +119,18 @@ class PathInput extends Component {
|
||||
const {
|
||||
className,
|
||||
name,
|
||||
value,
|
||||
paths,
|
||||
includeFiles,
|
||||
hasFileBrowser,
|
||||
onChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
value,
|
||||
isFileBrowserModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<AutoSuggestInput
|
||||
@@ -130,7 +146,7 @@ class PathInput extends Component {
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onChange={onChange}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
|
||||
{
|
||||
@@ -144,7 +160,7 @@ class PathInput extends Component {
|
||||
</FormInputButton>
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={this.state.isFileBrowserModalOpen}
|
||||
isOpen={isFileBrowserModalOpen}
|
||||
name={name}
|
||||
value={value}
|
||||
includeFiles={includeFiles}
|
||||
|
||||
@@ -20,7 +20,7 @@ function getType(type) {
|
||||
return inputTypes.NUMBER;
|
||||
case 'path':
|
||||
return inputTypes.PATH;
|
||||
case 'filepath':
|
||||
case 'filePath':
|
||||
return inputTypes.PATH;
|
||||
case 'select':
|
||||
return inputTypes.SELECT;
|
||||
@@ -28,7 +28,7 @@ function getType(type) {
|
||||
return inputTypes.TEXT_TAG;
|
||||
case 'textbox':
|
||||
return inputTypes.TEXT;
|
||||
case 'oauth':
|
||||
case 'oAuth':
|
||||
return inputTypes.OAUTH;
|
||||
default:
|
||||
return inputTypes.TEXT;
|
||||
@@ -60,6 +60,7 @@ function ProviderFieldFormGroup(props) {
|
||||
value,
|
||||
type,
|
||||
advanced,
|
||||
hidden,
|
||||
pending,
|
||||
errors,
|
||||
warnings,
|
||||
@@ -68,6 +69,13 @@ function ProviderFieldFormGroup(props) {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
if (
|
||||
hidden === 'hidden' ||
|
||||
(hidden === 'hiddenIfNotSet' && !value)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
@@ -86,7 +94,7 @@ function ProviderFieldFormGroup(props) {
|
||||
errors={errors}
|
||||
warnings={warnings}
|
||||
pending={pending}
|
||||
includeFiles={type === 'filepath' ? true : undefined}
|
||||
includeFiles={type === 'filePath' ? true : undefined}
|
||||
onChange={onChange}
|
||||
{...otherProps}
|
||||
/>
|
||||
@@ -108,6 +116,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||
value: PropTypes.any,
|
||||
type: PropTypes.string.isRequired,
|
||||
advanced: PropTypes.bool.isRequired,
|
||||
hidden: PropTypes.string,
|
||||
pending: PropTypes.bool.isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -13,7 +12,7 @@ function createMapStateToProps() {
|
||||
(state) => state.rootFolders,
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(rootFolders, includeNoChange) => {
|
||||
const values = _.map(rootFolders.items, (rootFolder) => {
|
||||
const values = rootFolders.items.map((rootFolder) => {
|
||||
return {
|
||||
key: rootFolder.path,
|
||||
value: rootFolder.path,
|
||||
@@ -85,7 +84,7 @@ class RootFolderSelectInputConnector extends Component {
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
if (!value || !_.some(values, (v) => v.key === value) || value === ADD_NEW_KEY) {
|
||||
if (!value || !values.some((v) => v.key === value) || value === ADD_NEW_KEY) {
|
||||
const defaultValue = values[0];
|
||||
|
||||
if (defaultValue.key === ADD_NEW_KEY) {
|
||||
@@ -96,6 +95,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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.seriesFolder {
|
||||
flex: 0 0 auto;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.freeSpace {
|
||||
margin-left: 15px;
|
||||
color: $darkGray;
|
||||
|
||||
@@ -7,14 +7,20 @@ import styles from './RootFolderSelectInputOption.css';
|
||||
|
||||
function RootFolderSelectInputOption(props) {
|
||||
const {
|
||||
id,
|
||||
value,
|
||||
freeSpace,
|
||||
seriesFolder,
|
||||
isMobile,
|
||||
isWindows,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const slashCharacter = isWindows ? '\\' : '/';
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
isMobile={isMobile}
|
||||
{...otherProps}
|
||||
>
|
||||
@@ -23,7 +29,18 @@ function RootFolderSelectInputOption(props) {
|
||||
isMobile && styles.isMobile
|
||||
)}
|
||||
>
|
||||
<div>{value}</div>
|
||||
<div className={styles.value}>
|
||||
{value}
|
||||
|
||||
{
|
||||
seriesFolder && id !== 'addNew' ?
|
||||
<div className={styles.seriesFolder}>
|
||||
{slashCharacter}
|
||||
{seriesFolder}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
freeSpace != null &&
|
||||
@@ -37,9 +54,12 @@ function RootFolderSelectInputOption(props) {
|
||||
}
|
||||
|
||||
RootFolderSelectInputOption.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
isMobile: PropTypes.bool.isRequired
|
||||
seriesFolder: PropTypes.string,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool
|
||||
};
|
||||
|
||||
export default RootFolderSelectInputOption;
|
||||
|
||||
@@ -7,10 +7,20 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pathContainer {
|
||||
@add-mixin truncate;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.path {
|
||||
@add-mixin truncate;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
flex: 1 0 0;
|
||||
.seriesFolder {
|
||||
flex: 0 1 auto;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.freeSpace {
|
||||
|
||||
@@ -8,17 +8,32 @@ function RootFolderSelectInputSelectedValue(props) {
|
||||
const {
|
||||
value,
|
||||
freeSpace,
|
||||
seriesFolder,
|
||||
includeFreeSpace,
|
||||
isWindows,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const slashCharacter = isWindows ? '\\' : '/';
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputSelectedValue
|
||||
className={styles.selectedValue}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.path}>
|
||||
{value}
|
||||
<div className={styles.pathContainer}>
|
||||
<div className={styles.path}>
|
||||
{value}
|
||||
</div>
|
||||
|
||||
{
|
||||
seriesFolder ?
|
||||
<div className={styles.seriesFolder}>
|
||||
{slashCharacter}
|
||||
{seriesFolder}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -34,6 +49,8 @@ function RootFolderSelectInputSelectedValue(props) {
|
||||
RootFolderSelectInputSelectedValue.propTypes = {
|
||||
value: PropTypes.string,
|
||||
freeSpace: PropTypes.number,
|
||||
seriesFolder: PropTypes.string,
|
||||
isWindows: PropTypes.bool,
|
||||
includeFreeSpace: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
const seriesTypeOptions = [
|
||||
{ key: 'standard', value: 'Standard' },
|
||||
{ key: 'daily', value: 'Daily' },
|
||||
{ key: 'anime', value: 'Anime' }
|
||||
{ key: seriesTypes.STANDARD, value: 'Standard' },
|
||||
{ key: seriesTypes.DAILY, value: 'Daily' },
|
||||
{ key: seriesTypes.ANIME, value: 'Anime' }
|
||||
];
|
||||
|
||||
function SeriesTypeSelectInput(props) {
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
min-width: 20%;
|
||||
max-width: 100%;
|
||||
width: 0%;
|
||||
height: 21px;
|
||||
height: 31px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ class TagInput extends Component {
|
||||
className={styles.internalInput}
|
||||
inputContainerClassName={classNames(
|
||||
inputContainerClassName,
|
||||
isFocused && styles.isFocused,
|
||||
isFocused && styles.isFocused
|
||||
)}
|
||||
value={value}
|
||||
suggestions={suggestions}
|
||||
@@ -260,7 +260,7 @@ TagInput.propTypes = {
|
||||
minQueryLength: PropTypes.number.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
tagComponent: PropTypes.func.isRequired,
|
||||
tagComponent: PropTypes.elementType.isRequired,
|
||||
onTagAdd: PropTypes.func.isRequired,
|
||||
onTagDelete: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px 16px;
|
||||
padding: 1px 16px;
|
||||
min-height: 33px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ TagInputInput.propTypes = {
|
||||
inputProps: PropTypes.object.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
isFocused: PropTypes.bool.isRequired,
|
||||
tagComponent: PropTypes.func.isRequired,
|
||||
tagComponent: PropTypes.elementType.isRequired,
|
||||
onTagDelete: PropTypes.func.isRequired,
|
||||
onInputContainerPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
5
frontend/src/Components/Form/TagInputTag.css
Normal file
5
frontend/src/Components/Form/TagInputTag.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.tag {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
height: 31px;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { kinds } from 'Helpers/Props';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './TagInputTag.css';
|
||||
|
||||
class TagInputTag extends Component {
|
||||
|
||||
@@ -31,9 +32,9 @@ class TagInputTag extends Component {
|
||||
tag,
|
||||
kind
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.tag}
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
|
||||
@@ -95,8 +95,8 @@ FilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
buttonComponent: PropTypes.func.isRequired,
|
||||
filterModalConnectorComponent: PropTypes.func,
|
||||
buttonComponent: PropTypes.elementType.isRequired,
|
||||
filterModalConnectorComponent: PropTypes.elementType,
|
||||
filterModalConnectorComponentProps: PropTypes.object,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ class Menu extends Component {
|
||||
|
||||
this._scheduleUpdate = null;
|
||||
this._menuButtonId = getUniqueElememtId();
|
||||
this._menuContentId = getUniqueElememtId();
|
||||
|
||||
this.state = {
|
||||
isMenuOpen: false,
|
||||
@@ -99,12 +100,14 @@ class Menu extends Component {
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
window.addEventListener('scroll', this.onWindowScroll, { capture: true });
|
||||
window.addEventListener('click', this.onWindowClick);
|
||||
window.addEventListener('touchstart', this.onTouchStart);
|
||||
}
|
||||
|
||||
_removeListener() {
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
window.removeEventListener('scroll', this.onWindowScroll, { capture: true });
|
||||
window.removeEventListener('click', this.onWindowClick);
|
||||
window.removeEventListener('touchstart', this.onTouchStart);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -123,6 +126,30 @@ class Menu extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onTouchStart = (event) => {
|
||||
const menuButton = document.getElementById(this._menuButtonId);
|
||||
const menuContent = document.getElementById(this._menuContentId);
|
||||
|
||||
if (!menuButton || !menuContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.targetTouches.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.targetTouches[0].target;
|
||||
|
||||
if (
|
||||
!menuButton.contains(target) &&
|
||||
!menuContent.contains(target) &&
|
||||
this.state.isMenuOpen
|
||||
) {
|
||||
this.setState({ isMenuOpen: false });
|
||||
this._removeListener();
|
||||
}
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
this.setMaxHeight();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getUniqueElementId from 'Utilities/getUniqueElementId';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import styles from './MenuContent.css';
|
||||
|
||||
@@ -12,6 +13,7 @@ class MenuContent extends Component {
|
||||
const {
|
||||
forwardedRef,
|
||||
className,
|
||||
id,
|
||||
children,
|
||||
style,
|
||||
isOpen
|
||||
@@ -19,6 +21,7 @@ class MenuContent extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
ref={forwardedRef}
|
||||
className={className}
|
||||
style={style}
|
||||
@@ -38,13 +41,15 @@ class MenuContent extends Component {
|
||||
MenuContent.propTypes = {
|
||||
forwardedRef: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
isOpen: PropTypes.bool
|
||||
};
|
||||
|
||||
MenuContent.defaultProps = {
|
||||
className: styles.menuContent
|
||||
className: styles.menuContent,
|
||||
id: getUniqueElementId()
|
||||
};
|
||||
|
||||
export default MenuContent;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.menuItem {
|
||||
@add-mixin truncate;
|
||||
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
padding: 10px 20px;
|
||||
@@ -17,3 +16,8 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: $disabledColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './MenuItem.css';
|
||||
|
||||
@@ -12,12 +13,17 @@ class MenuItem extends Component {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
isDisabled,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={className}
|
||||
className={classNames(
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
@@ -28,11 +34,13 @@ class MenuItem extends Component {
|
||||
|
||||
MenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired
|
||||
children: PropTypes.node.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
MenuItem.defaultProps = {
|
||||
className: styles.menuItem
|
||||
className: styles.menuItem,
|
||||
isDisabled: false
|
||||
};
|
||||
|
||||
export default MenuItem;
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.modalOpenIOS {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sizes
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,8 @@ import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import elementClass from 'element-class';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isIOS } from 'Utilities/mobile';
|
||||
import { setScrollLock } from 'Utilities/scrollLock';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||
@@ -31,6 +33,7 @@ class Modal extends Component {
|
||||
this._node = document.getElementById('portal-root');
|
||||
this._backgroundRef = null;
|
||||
this._modalId = getUniqueElememtId();
|
||||
this._bodyScrollTop = 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -69,7 +72,14 @@ class Modal extends Component {
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
|
||||
if (openModals.length === 1) {
|
||||
elementClass(document.body).add(styles.modalOpen);
|
||||
if (isIOS()) {
|
||||
setScrollLock(true);
|
||||
const scrollTop = document.body.scrollTop;
|
||||
this._bodyScrollTop = scrollTop;
|
||||
elementClass(document.body).add(styles.modalOpenIOS);
|
||||
} else {
|
||||
elementClass(document.body).add(styles.modalOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +88,14 @@ class Modal extends Component {
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
|
||||
if (openModals.length === 0) {
|
||||
elementClass(document.body).remove(styles.modalOpen);
|
||||
setScrollLock(false);
|
||||
|
||||
if (isIOS()) {
|
||||
elementClass(document.body).remove(styles.modalOpenIOS);
|
||||
document.body.scrollTop = this._bodyScrollTop;
|
||||
} else {
|
||||
elementClass(document.body).remove(styles.modalOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ ModalBody.propTypes = {
|
||||
className: PropTypes.string,
|
||||
innerClassName: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL])
|
||||
scrollDirection: PropTypes.oneOf(scrollDirections.all)
|
||||
};
|
||||
|
||||
ModalBody.defaultProps = {
|
||||
|
||||
@@ -7,7 +7,7 @@ import styles from './MonitorToggleButton.css';
|
||||
|
||||
function getTooltip(monitored, isDisabled) {
|
||||
if (isDisabled) {
|
||||
return 'Cannot toogle monitored state when series is unmonitored';
|
||||
return 'Cannot toggle monitored state when series is unmonitored';
|
||||
}
|
||||
|
||||
if (monitored) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
@@ -7,6 +8,17 @@ import styles from './PageContentBody.css';
|
||||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onScroll = (props) => {
|
||||
const { onScroll } = this.props;
|
||||
|
||||
if (this.props.onScroll && !isLocked()) {
|
||||
onScroll(props);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -27,6 +39,7 @@ class PageContentBody extends Component {
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.VERTICAL}
|
||||
{...otherProps}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<div className={innerClassName}>
|
||||
{children}
|
||||
@@ -41,6 +54,7 @@ PageContentBody.propTypes = {
|
||||
innerClassName: PropTypes.string,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onScroll: PropTypes.func,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -14,4 +14,4 @@ function createMapStateToProps() {
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(PageContentBody);
|
||||
export default connect(createMapStateToProps, null, null, { forwardRef: true })(PageContentBody);
|
||||
|
||||
@@ -94,7 +94,7 @@ PageSidebarItem.propTypes = {
|
||||
isActiveParent: PropTypes.bool,
|
||||
isParentItem: PropTypes.bool.isRequired,
|
||||
isChildItem: PropTypes.bool.isRequired,
|
||||
statusComponent: PropTypes.func,
|
||||
statusComponent: PropTypes.elementType,
|
||||
children: PropTypes.node,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.thumb {
|
||||
min-height: 50px;
|
||||
min-height: 100px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
background-color: $scrollbarBackgroundColor;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Scrollbars } from 'react-custom-scrollbars';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import styles from './OverlayScroller.css';
|
||||
|
||||
const SCROLLBAR_SIZE = 10;
|
||||
|
||||
class OverlayScroller extends Component {
|
||||
|
||||
//
|
||||
@@ -21,7 +23,11 @@ class OverlayScroller extends Component {
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
if (!this._isScrolling && scrollTop != null && scrollTop !== prevProps.scrollTop) {
|
||||
if (
|
||||
!this._isScrolling &&
|
||||
scrollTop != null &&
|
||||
scrollTop !== prevProps.scrollTop
|
||||
) {
|
||||
this._scroller.scrollTop(scrollTop);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +48,42 @@ class OverlayScroller extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
_renderTrackHorizontal = ({ style, props }) => {
|
||||
const finalStyle = {
|
||||
...style,
|
||||
right: 2,
|
||||
bottom: 2,
|
||||
left: 2,
|
||||
borderRadius: 3,
|
||||
height: SCROLLBAR_SIZE
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={finalStyle}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_renderTrackVertical = ({ style, props }) => {
|
||||
const finalStyle = {
|
||||
...style,
|
||||
right: 2,
|
||||
bottom: 2,
|
||||
top: 2,
|
||||
borderRadius: 3,
|
||||
width: SCROLLBAR_SIZE
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={finalStyle}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_renderView = (props) => {
|
||||
return (
|
||||
<div
|
||||
@@ -91,6 +133,8 @@ class OverlayScroller extends Component {
|
||||
ref={this._setScrollRef}
|
||||
autoHide={autoHide}
|
||||
hideTracksWhenNotNeeded={autoScroll}
|
||||
renderTrackHorizontal={this._renderTrackHorizontal}
|
||||
renderTrackVertical={this._renderTrackVertical}
|
||||
renderThumbHorizontal={this._renderThumb}
|
||||
renderThumbVertical={this._renderThumb}
|
||||
renderView={this._renderView}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@add-mixin scrollbar;
|
||||
@add-mixin scrollbarTrack;
|
||||
@add-mixin scrollbarThumb;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.none {
|
||||
@@ -26,3 +27,11 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.both {
|
||||
overflow: scroll;
|
||||
|
||||
&.autoScroll {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class Scroller extends Component {
|
||||
|
||||
Scroller.propTypes = {
|
||||
className: PropTypes.string,
|
||||
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired,
|
||||
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
|
||||
autoScroll: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
|
||||
@@ -9,6 +9,7 @@ import titleCase from 'Utilities/String/titleCase';
|
||||
import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
|
||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
||||
import { fetchSeries } from 'Store/Actions/seriesActions';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
@@ -72,6 +73,7 @@ const mapDispatchToProps = {
|
||||
dispatchFetchQueue: fetchQueue,
|
||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchFetchSeries: fetchSeries,
|
||||
dispatchFetchTags: fetchTags,
|
||||
dispatchFetchTagDetails: fetchTagDetails
|
||||
};
|
||||
@@ -260,7 +262,7 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
|
||||
handleSystemTask = () => {
|
||||
// No-op for now, we may want this later
|
||||
this.props.dispatchFetchCommands();
|
||||
}
|
||||
|
||||
handleRootfolder = () => {
|
||||
@@ -288,6 +290,7 @@ class SignalRConnector extends Component {
|
||||
|
||||
const {
|
||||
dispatchFetchCommands,
|
||||
dispatchFetchSeries,
|
||||
dispatchSetAppValue
|
||||
} = this.props;
|
||||
|
||||
@@ -295,6 +298,7 @@ class SignalRConnector extends Component {
|
||||
// are in sync after reconnecting.
|
||||
|
||||
if (this.props.isReconnecting || this.props.isDisconnected) {
|
||||
dispatchFetchSeries();
|
||||
dispatchFetchCommands();
|
||||
repopulatePage();
|
||||
}
|
||||
@@ -376,6 +380,7 @@ SignalRConnector.propTypes = {
|
||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchFetchSeries: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ RelativeDateCell.propTypes = {
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
component: PropTypes.func,
|
||||
component: PropTypes.elementType,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.tableContainer {
|
||||
overflow-x: auto;
|
||||
&.horizontalScroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
@@ -10,7 +12,12 @@
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.tableContainer {
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
|
||||
&.horizontalScroll {
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { icons, scrollDirections } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
@@ -28,6 +29,7 @@ function getTableHeaderCellProps(props) {
|
||||
function Table(props) {
|
||||
const {
|
||||
className,
|
||||
horizontalScroll,
|
||||
selectAll,
|
||||
columns,
|
||||
optionsComponent,
|
||||
@@ -41,14 +43,22 @@ function Table(props) {
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
className={styles.tableContainer}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
className={classNames(
|
||||
styles.tableContainer,
|
||||
horizontalScroll && styles.horizontalScroll
|
||||
)}
|
||||
scrollDirection={
|
||||
horizontalScroll ?
|
||||
scrollDirections.HORIZONTAL :
|
||||
scrollDirections.NONE
|
||||
}
|
||||
>
|
||||
<table className={className}>
|
||||
<TableHeader>
|
||||
{
|
||||
selectAll &&
|
||||
<TableSelectAllHeaderCell {...otherProps} />
|
||||
selectAll ?
|
||||
<TableSelectAllHeaderCell {...otherProps} /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -111,9 +121,10 @@ function Table(props) {
|
||||
|
||||
Table.propTypes = {
|
||||
className: PropTypes.string,
|
||||
horizontalScroll: PropTypes.bool.isRequired,
|
||||
selectAll: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
optionsComponent: PropTypes.func,
|
||||
optionsComponent: PropTypes.elementType,
|
||||
pageSize: PropTypes.number,
|
||||
canModifyColumns: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
@@ -123,6 +134,7 @@ Table.propTypes = {
|
||||
|
||||
Table.defaultProps = {
|
||||
className: styles.table,
|
||||
horizontalScroll: true,
|
||||
selectAll: false
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
@@ -127,116 +127,118 @@ class TableOptionsModal extends Component {
|
||||
const isDraggingDown = isDragging && dropIndex > dragIndex;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
isOpen ?
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
isOpen ?
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Table Options
|
||||
</ModalHeader>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
{
|
||||
hasPageSize ?
|
||||
<FormGroup>
|
||||
<FormLabel>Page Size</FormLabel>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
{
|
||||
hasPageSize ?
|
||||
<FormGroup>
|
||||
<FormLabel>Page Size</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="pageSize"
|
||||
value={pageSize || 0}
|
||||
helpText="Number of items to show on each page"
|
||||
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
|
||||
onChange={this.onPageSizeChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
OptionsComponent ?
|
||||
<OptionsComponent
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
/> : null
|
||||
}
|
||||
|
||||
{
|
||||
canModifyColumns ?
|
||||
<FormGroup>
|
||||
<FormLabel>Columns</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text="Choose which columns are visible and which order they appear in"
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="pageSize"
|
||||
value={pageSize || 0}
|
||||
helpText="Number of items to show on each page"
|
||||
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
|
||||
onChange={this.onPageSizeChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className={styles.columns}>
|
||||
{
|
||||
columns.map((column, index) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
columnLabel,
|
||||
isVisible,
|
||||
isModifiable
|
||||
} = column;
|
||||
{
|
||||
OptionsComponent ?
|
||||
<OptionsComponent
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
/> : null
|
||||
}
|
||||
|
||||
{
|
||||
canModifyColumns ?
|
||||
<FormGroup>
|
||||
<FormLabel>Columns</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text="Choose which columns are visible and which order they appear in"
|
||||
/>
|
||||
|
||||
<div className={styles.columns}>
|
||||
{
|
||||
columns.map((column, index) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
columnLabel,
|
||||
isVisible,
|
||||
isModifiable
|
||||
} = column;
|
||||
|
||||
if (isModifiable !== false) {
|
||||
return (
|
||||
<TableOptionsColumnDragSource
|
||||
key={name}
|
||||
name={name}
|
||||
label={label || columnLabel}
|
||||
isVisible={isVisible}
|
||||
isModifiable={true}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
onVisibleChange={this.onVisibleChange}
|
||||
onColumnDragMove={this.onColumnDragMove}
|
||||
onColumnDragEnd={this.onColumnDragEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isModifiable !== false) {
|
||||
return (
|
||||
<TableOptionsColumnDragSource
|
||||
<TableOptionsColumn
|
||||
key={name}
|
||||
name={name}
|
||||
label={label || columnLabel}
|
||||
isVisible={isVisible}
|
||||
isModifiable={true}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
isModifiable={false}
|
||||
onVisibleChange={this.onVisibleChange}
|
||||
onColumnDragMove={this.onColumnDragMove}
|
||||
onColumnDragEnd={this.onColumnDragEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<TableOptionsColumn
|
||||
key={name}
|
||||
name={name}
|
||||
label={label || columnLabel}
|
||||
isVisible={isVisible}
|
||||
index={index}
|
||||
isModifiable={false}
|
||||
onVisibleChange={this.onVisibleChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<TableOptionsColumnDragPreview />
|
||||
<TableOptionsColumnDragPreview />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent> :
|
||||
null
|
||||
}
|
||||
</Modal>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent> :
|
||||
null
|
||||
}
|
||||
</Modal>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -246,7 +248,7 @@ TableOptionsModal.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
pageSize: PropTypes.number,
|
||||
canModifyColumns: PropTypes.bool.isRequired,
|
||||
optionsComponent: PropTypes.func,
|
||||
optionsComponent: PropTypes.elementType,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
@@ -255,4 +257,4 @@ TableOptionsModal.defaultProps = {
|
||||
canModifyColumns: true
|
||||
};
|
||||
|
||||
export default DragDropContext(HTML5Backend)(TableOptionsModal);
|
||||
export default TableOptionsModal;
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { WindowScroller } from 'react-virtualized';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import Measure from 'Components/Measure';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
@@ -51,10 +52,10 @@ class VirtualTable extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, preState) {
|
||||
const scrollIndex = this.props.scrollIndex;
|
||||
const { scrollIndex, rowHeight } = this.props;
|
||||
|
||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||
const scrollTop = (scrollIndex + 1) * ROW_HEIGHT + 20;
|
||||
const scrollTop = (scrollIndex + 1) * rowHeight + 20;
|
||||
|
||||
this.props.onScroll({ scrollTop });
|
||||
}
|
||||
@@ -83,6 +84,16 @@ class VirtualTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onScroll = (props) => {
|
||||
if (isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { onScroll } = this.props;
|
||||
|
||||
onScroll(props);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -107,7 +118,7 @@ class VirtualTable extends Component {
|
||||
<Measure onMeasure={this.onMeasure}>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
|
||||
onScroll={onScroll}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{({ height, isScrolling }) => {
|
||||
return (
|
||||
@@ -154,6 +165,7 @@ VirtualTable.propTypes = {
|
||||
header: PropTypes.node.isRequired,
|
||||
headerHeight: PropTypes.number.isRequired,
|
||||
rowRenderer: PropTypes.func.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
onRender: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user