1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-11 15:19:56 -04:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Taloth Saldono
40b34dbb5e Do custom gzip compression on mono when returning api responses to avoid a memory leak. 2019-01-08 20:34:35 +01:00
Taloth Saldono
567719e6c4 Only do the custom decompression on mono. 2019-01-08 20:33:50 +01:00
Taloth Saldono
76212ef908 Do our own decompression to workaround mono bug. 2019-01-04 23:29:32 +01:00
1506 changed files with 22917 additions and 34621 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
indent_style = space
indent_size = 4
[*.{js,html,js,hbs,less,css}]
[*.{js,html,js,hbs,less}]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

24
.gitattributes vendored
View File

@@ -1,12 +1,22 @@
# Auto detect text files and perform LF normalization
* 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
*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
View File

@@ -45,10 +45,6 @@ _dotCover*
# DevExpress CodeRush
src/.cr/
# Emacs
*~
\#*\#
# NCrunch
*.ncrunch*
.*crunch*.local.xml
@@ -119,9 +115,7 @@ node_modules/
_output*
_rawPackage/
_dotTrace*
_tests*
_publish*
_temp*
_tests/
*.Result.xml
setup/Output/
*.~is
@@ -139,5 +133,6 @@ output/*
.DS_Store
_start
_temp_*/**/*
src/.idea/

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "src/ExternalModules/CurlSharp"]
path = src/ExternalModules/CurlSharp
url = https://github.com/Sonarr/CurlSharp.git
branch = master

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
save-prefix=""

View File

@@ -1,2 +1 @@
save-exact true
registry "https://registry.yarnpkg.com"
save-prefix ""

View File

@@ -7,7 +7,21 @@ Setup guides, FAQ, the more information we have on the wiki the better.
## Development ##
See the readme for information on setting up your development environment.
### Tools required ###
- Visual Studio 2015
- HTML/Javascript editor of choice (Sublime Text/Webstorm/Atom/etc)
- npm (node package manager)
- git
### Getting started ###
1. Fork Sonarr
2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)*
3. Run `npm install`
4. Run `npm start` - Used to compile the UI components and copy them.
Leave this window open.
If you have gulp globally installed you can use `gulp watch` instead
5. Compile in Visual Studio
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)

View File

@@ -1,73 +1,52 @@
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
# Sonarr
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
## Getting Started
## Major Features Include:
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
## Support
- [Donate](https://sonarr.tv/donate)
- [Discord](https://discord.gg/M6BvZn5)
- [Reddit](https://www.reddit.com/r/sonarr)
## Features
### Current Features
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
- Automatically detects new episodes
- Can scan your existing library and download any missing episodes
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
- Automatic failed download handling will try another release if one fails
- Manual search so you can pick any release or to see why a release was not downloaded automatically
- Fully configurable episode renaming
- Full integration with SABnzbd and NZBGet
- Full integration with Kodi, Plex (notification, library update, metadata)
- Full support for specials and multi-episode releases
- And a beautiful UI
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically detects new episodes
* Can scan your existing library and download any missing episodes
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Fully configurable episode renaming
* Full integration with SABnzbd and NZBGet
* Full integration with Kodi, Plex (notification, library update, metadata)
* Full support for specials and multi-episode releases
* And a beautiful UI
## Configuring Development Environment:
### Requirements
- [Visual Studio 2017](https://www.visualstudio.com/vs)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download)
- [Yarn](https://yarnpkg.com)
* Visual Studio 2015 (https://www.visualstudio.com/vs/)
* [Git](https://git-scm.com/downloads)
* [NodeJS](https://nodejs.org/en/download/)
### Setup
- Make sure all the required software mentioned above are installed
- Clone the repository recursively to get Sonarr and it's submodules
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
- Install the required Node Packages using `yarn`
* Make sure all the required software mentioned above are installed.
* Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
* Grab the submodules `git submodule init && git submodule update`
* Install the required Node Packages `npm install`
* Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command.
### Backend Development
*Please note gulp must be running at all times while you are working with Sonarr client source files.*
- Run `yarn build` to build the UI
- Open `Sonarr.sln` in Visual Studio
- Make sure `Sonarr.Console` is set as the startup project
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
- Build Solution
### Development
### UI Development
* Open `NzbDrone.sln` in Visual Studio
* Make sure `NzbDrone.Console` is set as the startup project
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
- Run Sonarr.Console.exe (or debug in Visual Studio)
### License
### Licenses
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2020
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2017
### Sponsors
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
- [ReSharper](http://www.jetbrains.com/resharper/)
- [TeamCity](http://www.jetbrains.com/teamcity/)
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
* [ReSharper](http://www.jetbrains.com/resharper/)
* [WebStorm](http://www.jetbrains.com/webstorm/)
* [TeamCity](http://www.jetbrains.com/teamcity/)

185
build.sh
View File

@@ -1,16 +1,15 @@
#! /bin/bash
msBuildVersion='15.0'
outputFolder='./_output'
outputFolderWindows='./_output_windows'
outputFolderLinux='./_output_linux'
outputFolderMacOS='./_output_macos'
outputFolderMacOSApp='./_output_macos_app'
testPackageFolder='./_tests'
testPackageFolderWindows='./_tests_windows'
testPackageFolderLinux='./_tests_linux'
testPackageFolder='./_tests/'
testSearchPattern='*.Test/bin/x86/Release'
sourceFolder='./src'
slnFile=$sourceFolder/Sonarr.sln
updateSubFolder=Sonarr.Update
updateFolder=$outputFolder/Sonarr.Update
updateFolderMono=$outputFolderLinux/Sonarr.Update
nuget='tools/nuget/nuget.exe';
vswhere='tools/vswhere/vswhere.exe';
@@ -48,8 +47,7 @@ UpdateVersionNumber()
verBuild=`echo "${BUILD_NUMBER}" | cut -d. -f4`
BUILD_NUMBER=$verMajorMinorRevision.$verBuild
echo "##teamcity[buildNumber '$BUILD_NUMBER']"
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
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
fi
}
@@ -88,14 +86,13 @@ CleanFolder()
BuildWithMSBuild()
{
msBuildPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -find MSBuild\\\\\*\*\\\\Bin\\\\MSBuild.exe`
msBuildPath=${msBuildPath/C:\\/\/c\/}
msBuildPath=${msBuildPath//\\/\/}
msBuildDir=$(dirname "$msBuildPath")
installationPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -property installationPath`
installationPath=${installationPath/C:\\/\/c\/}
installationPath=${installationPath//\\/\/}
msBuild="$installationPath/MSBuild/$msBuildVersion/Bin"
echo $msBuild
echo $msBuildDir
export PATH=$msBuildDir:$PATH
export PATH=$msBuild:$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
@@ -104,15 +101,15 @@ BuildWithMSBuild()
BuildWithXbuild()
{
export MONO_IOMAP=case
CheckExitCode msbuild /t:Clean $slnFile
CheckExitCode xbuild /t:Clean $slnFile
mono $nuget restore $slnFile
CheckExitCode msbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
CheckExitCode xbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
}
LintUI()
{
ProgressStart 'ESLint'
CheckExitCode yarn lint
CheckExitCode yarn eslint
ProgressEnd 'ESLint'
ProgressStart 'Stylelint'
@@ -125,7 +122,6 @@ Build()
ProgressStart 'Build'
rm -rf $outputFolder
rm -rf $testPackageFolder
if [ $runtime = "dotnet" ] ; then
BuildWithMSBuild
@@ -171,48 +167,6 @@ 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'
@@ -236,14 +190,15 @@ 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
# 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 "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
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
rm $outputFolderLinux/Sonarr.exe*
@@ -255,7 +210,7 @@ PackageMono()
rm $outputFolderLinux/Sonarr.Windows.*
echo "Adding Sonarr.Mono to UpdatePackage"
cp $outputFolderLinux/Sonarr.Mono.* $outputFolderLinux/$updateSubFolder/
cp $outputFolderLinux/Sonarr.Mono.* $updateFolderMono
ProgressEnd 'Creating Mono Package'
}
@@ -311,92 +266,54 @@ PackageMacOSApp()
ProgressEnd 'Creating macOS App Package'
}
PackageTestsMono()
PackageTests()
{
ProgressStart 'Creating Mono Test Package'
ProgressStart 'Creating Test Package'
rm -rf $testPackageFolderLinux
rm -rf $testPackageFolder
mkdir $testPackageFolder
echo "Copying Binaries"
cp -r $testPackageFolder $testPackageFolderLinux
find $sourceFolder -path $testSearchPattern -exec cp -r -u -T "{}" $testPackageFolder \;
if [ $runtime = "dotnet" ] ; then
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
$nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
else
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
mono $nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
fi
echo "Creating MDBs"
CreateMdbs $testPackageFolderLinux
cp $outputFolder/*.dll $testPackageFolder
cp ./test.sh $testPackageFolder
echo "Removing PDBs"
find $testPackageFolderLinux -name "*.pdb" -exec rm "{}" \;
echo "Creating MDBs for tests"
CreateMdbs $testPackageFolder
PatchMono $testPackageFolderLinux
rm -f $testPackageFolder/*.log.config
CleanFolder $testPackageFolder true
echo "Adding Sonarr.Core.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolderLinux
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.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 "Adding CurlSharp.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder
cp ./test.sh $testPackageFolderLinux/
dos2unix $testPackageFolderLinux/test.sh
echo "Copying CurlSharp libraries"
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
echo "Removing Sonarr.Windows"
rm $testPackageFolderLinux/Sonarr.Windows.*
rm -f $testPackageFolderLinux/*.log.config
CleanFolder $testPackageFolderLinux true
ProgressEnd 'Creating Linux Test Package'
ProgressEnd 'Creating Test Package'
}
PackageTestsWindows()
CleanupWindowsPackage()
{
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
ProgressStart 'Cleaning Windows Package'
echo "Removing 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.*
rm -f $outputFolder/Sonarr.Mono.*
echo "Adding Sonarr.Windows to UpdatePackage"
cp $outputFolderWindows/Sonarr.Windows.* $outputFolderWindows/$updateSubFolder/
cp $outputFolder/Sonarr.Windows.* $updateFolder
ProgressEnd 'Creating Windows Package'
ProgressEnd 'Cleaning Windows Package'
}
PublishArtifacts()
@@ -404,11 +321,10 @@ PublishArtifacts()
ProgressStart 'Publishing Artifacts'
# Tests
echo "##teamcity[publishArtifacts '$testPackageFolderWindows/** => tests.windows.zip']"
echo "##teamcity[publishArtifacts '$testPackageFolderLinux/** => tests.linux.zip']"
echo "##teamcity[publishArtifacts '_tests/** => tests.zip']"
# Releases
echo "##teamcity[publishArtifacts '$outputFolderWindows/** => Sonarr.$BRANCH.$BUILD_NUMBER.windows.zip!Sonarr']"
echo "##teamcity[publishArtifacts '$outputFolder/** => 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']"
@@ -438,7 +354,6 @@ RunGulp
PackageMono
PackageMacOS
PackageMacOSApp
PackageTestsMono
PackageTestsWindows
PackageWindows
PackageTests
CleanupWindowsPackage
PublishArtifacts

View File

@@ -1,7 +0,0 @@
# 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

View File

@@ -19,11 +19,7 @@ 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
if [ -z "${TEST_OUTPUT}" ]; then
debuild -b
else
debuild -us -uc -b
fi
debuild -b
# Restore debian directory to the original files
rm -rf ./debian
@@ -36,28 +32,16 @@ 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
if [ -z "${TEST_OUTPUT}" ]; then
debuild -b
else
debuild -us -uc -b
fi
debuild -b
echo Moving stuff around
mv ../sonarr_*.deb ./
mv ../sonarr_*.changes ./
rm ../sonarr_*.build
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 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
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
echo running alien
alien -r -v ./*.deb

View File

@@ -7,16 +7,15 @@ 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 (>= 5.18),
libmono-cil-dev (>= 5.18),
cli-common-dev (>= 0.9+xamarin5)
mono-devel (>= 4.6),
libmono-cil-dev (>= 4.6),
cli-common-dev (>= 0.5.7)
Package: sonarr
Architecture: all
Provides: nzbdrone
Conflicts: nzbdrone
Replaces: nzbdrone
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.18), 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)
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}
Description: Internet PVR

View File

@@ -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

View File

@@ -95,7 +95,7 @@ chown -R $USER:$GROUP /usr/lib/sonarr
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
#BEGIN BUILTIN UPDATER
if [ "$UPDATER" = "BuiltIn" ]; then
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ]; then
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
touch $CONFDIR/update_required
chown $USER:$GROUP $CONFDIR/update_required
@@ -104,4 +104,4 @@ fi
#DEBHELPER#
exit 0
exit 0

View File

@@ -22,25 +22,10 @@ if [ $1 = "install" ]; then
fi
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
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
# 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
else
# We don't support auto migration for other startup methods, so bail.
# This leaves the sonarr package in an incomplete state.

View File

@@ -1,9 +1,17 @@
#!/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
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
# 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
%:
dh $@ --with=systemd --with=cli
@@ -12,7 +20,7 @@ EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr ms
override_dh_installinit:
true
# Sonarr likes debug symbols for logging
# Sonarr like debug symbols for logging
override_dh_clistrip:
override_dh_makeclilibs:

View File

@@ -1,2 +1,2 @@
ignores msbuild
ignores libmediainfo0v5
recommends libcurl3
ignores msbuild

View File

@@ -1,23 +0,0 @@
FROM ubuntu:xenial AS builder
ENV DEBIAN_FRONTEND noninteractive
ENV MONO_VERSION 5.18
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

View File

@@ -1,18 +0,0 @@
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

View File

@@ -1,22 +0,0 @@
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

View File

@@ -1,29 +0,0 @@
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 \
libmono-system-net-http4.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

View File

@@ -1,15 +0,0 @@
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

View File

@@ -1,121 +0,0 @@
opt_parallel=
opt_version=
opt_mode=both
while getopts 'pv:m:r?h' c
do
case $c in
p) opt_parallel=1 ;;
v) opt_version=$OPTARG ;;
m) opt_mode=$OPTARG ;;
r) opt_report=1 ;;
?|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"
printf " -r only report\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
MONO_VERSIONS=""
# Future versions
MONO_VERSIONS="$MONO_VERSIONS 6.10=preview-xenial"
# Semi-Supported versions
MONO_VERSIONS="$MONO_VERSIONS 6.8 6.6 6.4 6.0"
# Supported versions
MONO_VERSIONS="$MONO_VERSIONS 5.20 5.18"
# Legacy unsupported versions (but appear to work)
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12"
# Legacy unsupported versions
MONO_VERSIONS="$MONO_VERSIONS 5.10 5.8 5.4 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_report" != "1" ]; then
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
fi
grep "<test-run" ../../_tests_results/**/*.xml | sed -r 's/.*?mono-([0-9.]+(-s)?).*?_([IU]).*?\.xml.*?failed="([0-9]*)".*/\1\t\3:\tfailed \4/g' | sort -V -t.

View File

@@ -28,12 +28,6 @@
"react"
],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"filenames/match-exported": ["error"],
@@ -215,6 +209,7 @@
"lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }],
"max-depth": ["error", {"maximum": 5}],
"max-nested-callbacks": ["error", 4],
"max-params": ["error", 6],
"max-statements": "off",
"max-statements-per-line": ["error", { "max": 1 }],
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred", "DragDropContext", "DragLayer", "DragSource", "DropTarget"]}],

View File

@@ -24,7 +24,7 @@
"ignoreAtRules": [
"/^add\\-mixin$/",
"/^define\\-mixin$/"
]
]
}
],
"at-rule-no-vendor-prefix": true,

View File

@@ -1,35 +0,0 @@
const loose = true;
module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {
development: {
presets: [
['@babel/preset-react', { development: true }]
],
plugins: [
'babel-plugin-inline-classnames'
]
},
production: {
presets: [
'@babel/preset-react'
],
plugins: [
'babel-plugin-transform-react-remove-prop-types'
]
}
}
};

View File

@@ -1,17 +1,15 @@
const gulp = require('gulp');
const runSequence = require('run-sequence');
require('./clean');
require('./copy');
require('./webpack');
gulp.task('build',
gulp.series('clean',
gulp.parallel(
'webpack',
'copyHtml',
'copyFonts',
'copyImages'
)
)
);
gulp.task('build', () => {
return runSequence('clean', [
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
]);
});

View File

@@ -1,12 +1,23 @@
const path = require('path');
const gulp = require('gulp');
const print = require('gulp-print').default;
const cache = require('gulp-cached');
const livereload = require('gulp-livereload');
const paths = require('./helpers/paths.js');
var path = require('path');
var gulp = require('gulp');
var print = require('gulp-print').default;
var cache = require('gulp-cached');
var livereload = require('gulp-livereload');
var paths = require('./helpers/paths.js');
gulp.task('copyJs', () => {
return gulp.src(
[
path.join(paths.src.root, 'polyfills.js')
])
.pipe(cache('copyJs'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyHtml', () => {
return gulp.src(paths.src.html, { base: paths.src.root })
return gulp.src(paths.src.html)
.pipe(cache('copyHtml'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
@@ -15,20 +26,20 @@ gulp.task('copyHtml', () => {
gulp.task('copyFonts', () => {
return gulp.src(
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
path.join(paths.src.fonts, '**', '*.*')
)
.pipe(cache('copyFonts'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(gulp.dest(paths.dest.fonts))
.pipe(livereload());
});
gulp.task('copyImages', () => {
return gulp.src(
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
path.join(paths.src.images, '**', '*.*')
)
.pipe(cache('copyImages'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(gulp.dest(paths.dest.images))
.pipe(livereload());
});

View File

@@ -1,5 +1,8 @@
require('./build.js');
require('./clean.js');
require('./copy.js');
require('./imageMin.js');
require('./start.js');
require('./stripBom.js');
require('./watch.js');
require('./webpack.js');

View File

@@ -1,6 +1,6 @@
const colors = require('ansi-colors');
const gulpUtil = require('gulp-util');
module.exports = function errorHandler(error) {
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
gulpUtil.log(gulpUtil.colors.red(`Error (${error.plugin}): ${error.message}`));
this.emit('end');
};

View File

@@ -0,0 +1,15 @@
const path = require('path');
const rootPath = path.resolve(__dirname + '/../../src/');
module.exports = function(source) {
if (this.cacheable) {
this.cacheable();
}
const resourcePath = this.resourcePath.replace(rootPath, '');
const wrappedSource =`
<!-- begin ${resourcePath} -->
${source}
<!-- end ${resourcePath} -->`;
return wrappedSource;
};

View File

@@ -1,15 +1,15 @@
const root = './frontend/src';
const root = './frontend/src/';
const paths = {
src: {
root,
html: `${root}/*.html`,
scripts: `${root}/**/*.js`,
content: `${root}/Content/`,
fonts: `${root}/Content/Fonts/`,
images: `${root}/Content/Images/`,
html: root + '*.html',
scripts: root + '**/*.js',
content: root + 'Content/',
fonts: root + 'Content/Fonts/',
images: root + 'Content/Images/',
exclude: {
libs: `!${root}/JsLibraries/**`
libs: `!${root}JsLibraries/**`
}
},
dest: {

15
frontend/gulp/imageMin.js Normal file
View File

@@ -0,0 +1,15 @@
var gulp = require('gulp');
var print = require('gulp-print').default;
var paths = require('./helpers/paths.js');
gulp.task('imageMin', () => {
var imagemin = require('gulp-imagemin');
return gulp.src(paths.src.images)
.pipe(imagemin({
progressive: false,
optimizationLevel: 4,
svgoPlugins: [{ removeViewBox: false }]
}))
.pipe(print())
.pipe(gulp.dest(paths.src.content + 'Images/'));
});

104
frontend/gulp/start.js Normal file
View File

@@ -0,0 +1,104 @@
// will download and run sonarr (server) in a non-windows enviroment
// you can use this if you don't care about the server code and just want to work
// with the web code.
var http = require('http');
var gulp = require('gulp');
var fs = require('fs');
var targz = require('tar.gz');
var del = require('del');
var spawn = require('child_process').spawn;
function download(url, dest, cb) {
console.log('Downloading ' + url + ' to ' + dest);
var file = fs.createWriteStream(dest);
http.get(url, function(response) {
response.pipe(file);
file.on('finish', function() {
console.log('Download completed');
file.close(cb);
});
});
}
function getLatest(cb) {
var branch = 'develop';
process.argv.forEach(function(val) {
var branchMatch = /branch=([\S]*)/.exec(val);
if (branchMatch && branchMatch.length > 1) {
branch = branchMatch[1];
}
});
var url = 'http://services.sonarr.tv/v1/update/' + branch + '?os=osx';
console.log('Checking for latest version:', url);
http.get(url, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
var updatePackage = JSON.parse(data).updatePackage;
console.log('Latest version available: ' + updatePackage.version + ' Release Date: ' + updatePackage.releaseDate);
cb(updatePackage);
});
}).on('error', function(e) {
console.log('problem with request: ' + e.message);
});
}
function extract(source, dest, cb) {
console.log('extracting download page to ' + dest);
new targz().extract(source, dest, function(err) {
if (err) {
console.log(err);
}
console.log('Update package extracted.');
cb();
});
}
gulp.task('getSonarr', function() {
try {
fs.mkdirSync('./_start/');
} catch (e) {
if (e.code !== 'EEXIST') {
throw e;
}
}
getLatest(function(updatePackage) {
var packagePath = './_start/' + updatePackage.filename;
var dirName = './_start/' + updatePackage.version;
download(updatePackage.url, packagePath, function() {
extract(packagePath, dirName, function() {
// clean old binaries
console.log('Cleaning old binaries');
del.sync(['./_output/*', '!./_output/UI/']);
console.log('copying binaries to target');
gulp.src(dirName + '/NzbDrone/*.*')
.pipe(gulp.dest('./_output/'));
});
});
});
});
gulp.task('startSonarr', function() {
var ls = spawn('mono', ['--debug', './_output/NzbDrone.exe']);
ls.stdout.on('data', function(data) {
process.stdout.write(data);
});
ls.stderr.on('data', function(data) {
process.stdout.write(data);
});
ls.on('close', function(code) {
console.log('child process exited with code ' + code);
});
});

13
frontend/gulp/stripBom.js Normal file
View File

@@ -0,0 +1,13 @@
const gulp = require('gulp');
const paths = require('./helpers/paths.js');
const stripbom = require('gulp-stripbom');
function stripBom(dest) {
gulp.src([paths.src.scripts, paths.src.exclude.libs])
.pipe(stripbom({ showLog: false }))
.pipe(gulp.dest(dest));
}
gulp.task('stripBom', () => {
stripBom(paths.src.root);
});

View File

@@ -1,18 +1,27 @@
const gulp = require('gulp');
const livereload = require('gulp-livereload');
const gulpWatch = require('gulp-watch');
const watch = require('gulp-watch');
const paths = require('./helpers/paths.js');
require('./copy.js');
require('./webpack.js');
function watch() {
livereload.listen({ start: true });
gulp.task('webpackWatch')();
gulpWatch(paths.src.html, gulp.series('copyHtml'));
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
function watchTask(glob, task) {
const options = {
name: `watch: ${task}`,
verbose: true
};
return watch(glob, options, () => {
gulp.start(task);
});
}
gulp.task('watch', gulp.series('build', watch));
gulp.task('watch', ['copyHtml', 'copyFonts', 'copyImages', 'copyJs'], () => {
livereload.listen({ start: true });
gulp.start('webpackWatch');
watchTask(paths.src.html, 'copyHtml');
watchTask(`${paths.src.fonts}**/*.*`, 'copyFonts');
watchTask(`${paths.src.images}**/*.*`, 'copyImages');
});

View File

@@ -4,66 +4,57 @@ const livereload = require('gulp-livereload');
const path = require('path');
const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const root = path.join(__dirname, '..', 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const inlineWebWorkers = true;
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('ROOT:', root);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
'../src/Styles/Variables/dimensions',
'../src/Styles/Variables/fonts',
'../src/Styles/Variables/animations',
'../src/Styles/Variables/zIndexes'
'../src/Styles/Variables/animations'
].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 extractCSSPlugin = new ExtractTextPlugin({
filename: path.join('_output', uiFolder, 'Content', 'styles.css'),
allChunks: true,
disable: false,
ignoreOrder: true
});
const plugins = [
extractCSSPlugin,
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: path.join('Content', 'styles.css')
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.html',
filename: 'index.html'
})
];
if (isProduction) {
plugins.push(new UglifyJSPlugin({
sourceMap: true,
uglifyOptions: {
mangle: false,
output: {
comments: false,
beautify: true
}
}
}));
}
const config = {
mode: isProduction ? 'production' : 'development',
devtool: '#source-map',
stats: {
@@ -75,13 +66,15 @@ const config = {
},
entry: {
preload: 'preload.js',
vendor: 'vendor.js',
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
root,
path.join(root, 'Shims'),
'node_modules'
],
alias: {
@@ -90,22 +83,10 @@ const config = {
},
output: {
path: distFolder,
filename: '[name].js',
filename: path.join('_output', uiFolder, '[name].js'),
sourceMapFilename: '[file].map'
},
optimization: {
chunkIds: 'named',
splitChunks: {
chunks: 'initial'
}
},
performance: {
hints: false
},
plugins,
resolveLoader: {
@@ -117,76 +98,56 @@ const config = {
module: {
rules: [
{
test: /\.worker\.js$/,
issuer: {
// monaco-editor includes the editor.worker.js in other language workers,
// don't use worker-loader in that case
exclude: /monaco-editor/
},
use: {
loader: 'worker-loader',
options: {
name: '[name].js',
inline: inlineWebWorkers,
fallback: !inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
}
]
]
loader: 'babel-loader',
query: {
plugins: ['transform-class-properties'],
presets: ['es2015', 'decorators-legacy', 'react', 'stage-2'],
env: {
development: {
plugins: ['transform-react-jsx-source']
}
}
]
}
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
use: extractCSSPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-variables-loader',
options: {
cssVarsFiles
}
},
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]-[local]-[hash:base64:5]',
sourceMap: true
}
},
{
loader: 'postcss-loader',
options: {
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
}
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
}
}
]
]
})
},
// Global styles
@@ -233,35 +194,18 @@ const config = {
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
gulp.task('webpack', () => {
return webpackStream(config)
.pipe(gulp.dest('_output/UI'));
return gulp.src('index.js')
.pipe(webpackStream(config))
.pipe(gulp.dest(''));
});
gulp.task('webpackWatch', () => {
config.watch = true;
return webpackStream(config)
return gulp.src('')
.pipe(webpackStream(config))
.on('error', errorHandler)
.pipe(gulp.dest('_output/UI'))
.pipe(gulp.dest(''))
.on('error', errorHandler)
.pipe(livereload())
.on('error', errorHandler);

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"checkJs": false,
"baseUrl": "src",
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"paths": {
"*": [
"*"
]
}
},
"include": [
"./src/**/*"
],
"exclude": [
]
}

View File

@@ -14,8 +14,18 @@ module.exports = (ctx, configPath, options) => {
return Object.assign(acc, reload(vars));
}, {})
},
'postcss-color-function': {},
'postcss-nested': {}
'postcss-nested': {},
autoprefixer: {
browsers: [
'Chrome >= 30',
'Firefox >= 30',
'Safari >= 6',
'Edge >= 12',
'Explorer >= 11',
'iOS >= 7',
'Android >= 4.4'
]
}
}
};

View File

@@ -1,13 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { align, icons } from 'Helpers/Props';
import { icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -42,21 +41,9 @@ class Blacklist extends Component {
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -103,7 +90,7 @@ class Blacklist extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}

View File

@@ -105,14 +105,6 @@ class BlacklistConnector extends Component {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
//
// Render
@@ -134,7 +126,6 @@ class BlacklistConnector extends Component {
}
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,

View File

@@ -1,18 +1,18 @@
.language,
.quality {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}
.indexer {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 70px;
}

View File

@@ -1,5 +1,5 @@
.description {
composes: description from '~Components/DescriptionList/DescriptionListItemDescription.css';
composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css';
overflow-wrap: break-word;
}

View File

@@ -231,40 +231,6 @@ function HistoryDetails(props) {
</DescriptionList>
);
}
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
descriptionClassName={styles.description}
title="Name"
data={sourceTitle}
/>
</DescriptionList>
);
}
HistoryDetails.propTypes = {

View File

@@ -1,5 +1,5 @@
.markAsFailedButton {
composes: button from '~Components/Link/Button.css';
composes: button from 'Components/Link/Button.css';
margin-right: auto;
}

View File

@@ -23,8 +23,6 @@ function getHeaderTitle(eventType) {
return 'Episode File Deleted';
case 'episodeFileRenamed':
return 'Episode File Renamed';
case 'downloadIgnored':
return 'Download Ignored';
default:
return 'Unknown';
}

View File

@@ -5,10 +5,9 @@ import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -76,16 +75,6 @@ class History extends Component {
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
@@ -96,7 +85,7 @@ class History extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isFetchingAny && !isAllPopulated &&
<LoadingIndicator />
@@ -147,7 +136,7 @@ class History extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}

View File

@@ -8,7 +8,6 @@ import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions';
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
import History from './History';
function createMapStateToProps() {
@@ -29,8 +28,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
...historyActions,
fetchEpisodes,
clearEpisodes,
clearEpisodeFiles
clearEpisodes
};
class HistoryConnector extends Component {
@@ -70,7 +68,6 @@ class HistoryConnector extends Component {
unregisterPagePopulator(this.repopulate);
this.props.clearHistory();
this.props.clearEpisodes();
this.props.clearEpisodeFiles();
}
//
@@ -140,7 +137,6 @@ class HistoryConnector extends Component {
}
HistoryConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchHistory: PropTypes.func.isRequired,
gotoHistoryFirstPage: PropTypes.func.isRequired,
@@ -153,8 +149,7 @@ HistoryConnector.propTypes = {
setHistoryTableOption: PropTypes.func.isRequired,
clearHistory: PropTypes.func.isRequired,
fetchEpisodes: PropTypes.func.isRequired,
clearEpisodes: PropTypes.func.isRequired,
clearEpisodeFiles: PropTypes.func.isRequired
clearEpisodes: PropTypes.func.isRequired
};
export default withCurrentPage(

View File

@@ -1,5 +1,5 @@
.cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 35px;
text-align: center;

View File

@@ -19,8 +19,6 @@ function getIconName(eventType) {
return icons.DELETE;
case 'episodeFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
return icons.IGNORE;
default:
return icons.UNKNOWN;
}
@@ -49,8 +47,6 @@ function getTooltip(eventType, data) {
return 'Episode file deleted';
case 'episodeFileRenamed':
return 'Episode file renamed';
case 'downloadIgnored':
return 'Episode Download Ignored';
default:
return 'Unknown event';
}

View File

@@ -1,23 +1,23 @@
.downloadClient {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 120px;
}
.indexer {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 110px;
}
.details {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 30px;
}

View File

@@ -1,12 +1,12 @@
.torrent {
composes: label from '~Components/Label.css';
composes: label from 'Components/Label.css';
border-color: $torrentColor;
background-color: $torrentColor;
}
.usenet {
composes: label from '~Components/Label.css';
composes: label from 'Components/Label.css';
border-color: $usenetColor;
background-color: $usenetColor;

View File

@@ -1,24 +1,21 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons } from 'Helpers/Props';
import { icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
@@ -37,34 +34,37 @@ class Queue extends Component {
lastToggled: null,
selectedState: {},
isPendingSelected: false,
isConfirmRemoveModalOpen: false,
items: props.items
isConfirmRemoveModalOpen: false
};
}
componentDidUpdate(prevProps) {
const {
items,
isEpisodesFetching
} = this.props;
shouldComponentUpdate(nextProps) {
// Don't update when fetching has completed if items have changed,
// before episodes start fetching or when episodes start fetching.
if (
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
(
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items)
) ||
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return false;
}
return true;
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState({ selectedState: {} });
return;
}
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) {
@@ -100,8 +100,8 @@ class Queue extends Component {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = (payload) => {
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
onRemoveSelectedConfirmed = (blacklist) => {
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
this.setState({ isConfirmRemoveModalOpen: false });
}
@@ -117,6 +117,7 @@ class Queue extends Component {
isFetching,
isPopulated,
error,
items,
isEpisodesFetching,
isEpisodesPopulated,
episodesError,
@@ -124,7 +125,7 @@ class Queue extends Component {
totalRecords,
isGrabbing,
isRemoving,
isRefreshMonitoredDownloadsExecuting,
isCheckForFinishedDownloadExecuting,
onRefreshPress,
...otherProps
} = this.props;
@@ -134,15 +135,13 @@ class Queue extends Component {
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isPendingSelected,
items
isPendingSelected
} = this.state;
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
const hasError = error || episodesError;
const selectedIds = this.getSelectedIds();
const selectedCount = selectedIds.length;
const selectedCount = this.getSelectedIds().length;
const disableSelectedActions = selectedCount === 0;
return (
@@ -174,24 +173,9 @@ class Queue extends Component {
onPress={this.onRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
>
<TableOptionsModalWrapper
columns={columns}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
@@ -248,18 +232,11 @@ class Queue extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
<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}
/>
@@ -280,7 +257,7 @@ Queue.propTypes = {
totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired

View File

@@ -18,13 +18,13 @@ function createMapStateToProps() {
(state) => state.episodes,
(state) => state.queue.options,
(state) => state.queue.paged,
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => {
return {
isEpisodesFetching: episodes.isFetching,
isEpisodesPopulated: episodes.isPopulated,
episodesError: episodes.error,
isRefreshMonitoredDownloadsExecuting,
isCheckForFinishedDownloadExecuting,
...options,
...queue
};
@@ -129,7 +129,7 @@ class QueueConnector extends Component {
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
});
}
@@ -137,8 +137,8 @@ class QueueConnector extends Component {
this.props.grabQueueItems({ ids });
}
onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload);
onRemoveSelectedPress = (ids, blacklist) => {
this.props.removeQueueItems({ ids, blacklist });
}
//
@@ -164,8 +164,6 @@ class QueueConnector extends Component {
}
QueueConnector.propTypes = {
includeUnknownSeriesItems: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired,

View File

@@ -10,13 +10,13 @@ function QueueDetails(props) {
size,
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
status: queueStatus,
errorMessage,
progressBar
} = props;
const status = queueStatus.toLowerCase();
const progress = (100 - sizeleft / size * 100);
if (status === 'pending') {
@@ -39,35 +39,7 @@ function QueueDetails(props) {
);
}
if (trackedDownloadStatus === 'warning') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={'Downloaded - Unable to Import: check logs for details'}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Waiting to Import'}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Importing'}
/>
);
}
// TODO: show an icon when download is complete, but not imported yet?
}
if (errorMessage) {
@@ -118,8 +90,6 @@ QueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};

View File

@@ -1,23 +1,23 @@
.quality {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.protocol {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}
.progress {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 70px;
}

View File

@@ -10,7 +10,6 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
@@ -68,16 +67,13 @@ class QueueRow extends Component {
title,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
series,
episode,
language,
quality,
protocol,
indexer,
outputPath,
downloadClient,
estimatedCompletionTime,
timeleft,
@@ -101,8 +97,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>
@@ -130,7 +126,6 @@ class QueueRow extends Component {
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
/>
@@ -209,26 +204,12 @@ class QueueRow extends Component {
);
}
if (name === 'language') {
return (
<TableRowCell key={name}>
<EpisodeLanguage
language={language}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
{
quality ?
<EpisodeQuality
quality={quality}
/> :
null
}
<EpisodeQuality
quality={quality}
/>
</TableRowCell>
);
}
@@ -259,22 +240,6 @@ class QueueRow extends Component {
);
}
if (name === 'title') {
return (
<TableRowCell key={name}>
{title}
</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
{outputPath}
</TableRowCell>
);
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
@@ -356,7 +321,6 @@ class QueueRow extends Component {
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!series}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
@@ -372,16 +336,13 @@ 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,
episode: PropTypes.object,
language: PropTypes.object.isRequired,
quality: PropTypes.object.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
outputPath: PropTypes.string,
downloadClient: PropTypes.string,
estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string,

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -14,11 +15,11 @@ function createMapStateToProps() {
createEpisodeSelector(),
createUISettingsSelector(),
(series, episode, uiSettings) => {
const result = {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
const result = _.pick(uiSettings, [
'showRelativeDates',
'shortDateFormat',
'timeFormat'
]);
result.series = series;
result.episode = episode;
@@ -42,8 +43,8 @@ class QueueRowConnector extends Component {
this.props.grabQueueItem({ id: this.props.id });
}
onRemoveQueueItemPress = (payload) => {
this.props.removeQueueItem({ id: this.props.id, ...payload });
onRemoveQueueItemPress = (blacklist) => {
this.props.removeQueueItem({ id: this.props.id, blacklist });
}
//

View File

@@ -1,5 +1,5 @@
.status {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 30px;
}

View File

@@ -37,79 +37,63 @@ function QueueStatusCell(props) {
const {
sourceTitle,
status,
trackedDownloadStatus,
trackedDownloadState,
trackedDownloadStatus = 'Ok',
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;
let iconKind = kinds.DEFAULT;
let title = 'Downloading';
if (status === 'paused') {
iconName = icons.PAUSED;
title = 'Paused';
}
if (status === 'queued') {
iconName = icons.QUEUED;
title = 'Queued';
}
if (status === 'completed') {
iconName = icons.DOWNLOADED;
title = 'Downloaded';
if (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ' - Importing';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'delay') {
if (status === 'Paused') {
iconName = icons.PAUSED;
title = 'Paused';
}
if (status === 'Queued') {
iconName = icons.QUEUED;
title = 'Queued';
}
if (status === 'Completed') {
iconName = icons.DOWNLOADED;
title = 'Downloaded';
}
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}`;
@@ -132,7 +116,6 @@ function QueueStatusCell(props) {
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={tooltipPositions.RIGHT}
canFlip={false}
/>
</TableRowCell>
);
@@ -141,15 +124,9 @@ function QueueStatusCell(props) {
QueueStatusCell.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string
};
QueueStatusCell.defaultProps = {
trackedDownloadStatus: 'Ok',
trackedDownloadState: 'Downloading'
};
export default QueueStatusCell;

View File

@@ -0,0 +1,3 @@
.message {
margin-bottom: 30px;
}

View File

@@ -10,6 +10,7 @@ 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 {
@@ -20,41 +21,26 @@ 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 });
}
onRemoveConfirmed = () => {
const state = this.state;
onRemoveQueueItemConfirmed = () => {
const blacklist = this.state.blacklist;
this.resetState();
this.props.onRemovePress(state);
this.setState({ blacklist: false });
this.props.onRemovePress(blacklist);
}
onModalClose = () => {
this.resetState();
this.setState({ blacklist: false });
this.props.onModalClose();
}
@@ -64,11 +50,10 @@ class RemoveQueueItemModal extends Component {
render() {
const {
isOpen,
sourceTitle,
canIgnore
sourceTitle
} = this.props;
const { remove, blacklist } = this.state;
const blacklist = this.state.blacklist;
return (
<Modal
@@ -84,31 +69,17 @@ class RemoveQueueItemModal extends Component {
</ModalHeader>
<ModalBody>
<div>
<div className={styles.message}>
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="Starts a search for this episode again and prevents this release from being grabbed again"
helpText="Prevents Sonarr from automatically grabbing this episode again"
onChange={this.onBlacklistChange}
/>
</FormGroup>
@@ -122,7 +93,7 @@ class RemoveQueueItemModal extends Component {
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
onPress={this.onRemoveQueueItemConfirmed}
>
Remove
</Button>
@@ -136,7 +107,6 @@ 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
};

View File

@@ -21,41 +21,26 @@ class RemoveQueueItemsModal 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 });
}
// Listeners
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveConfirmed = () => {
const state = this.state;
onRemoveQueueItemConfirmed = () => {
const blacklist = this.state.blacklist;
this.resetState();
this.props.onRemovePress(state);
this.setState({ blacklist: false });
this.props.onRemovePress(blacklist);
}
onModalClose = () => {
this.resetState();
this.setState({ blacklist: false });
this.props.onModalClose();
}
@@ -65,11 +50,10 @@ class RemoveQueueItemsModal extends Component {
render() {
const {
isOpen,
selectedCount,
canIgnore
selectedCount
} = this.props;
const { remove, blacklist } = this.state;
const blacklist = this.state.blacklist;
return (
<Modal
@@ -90,23 +74,7 @@ class RemoveQueueItemsModal extends Component {
</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{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
@@ -125,7 +93,7 @@ class RemoveQueueItemsModal extends Component {
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
onPress={this.onRemoveQueueItemConfirmed}
>
Remove
</Button>
@@ -139,7 +107,6 @@ 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
};

View File

@@ -12,12 +12,8 @@ function createMapStateToProps() {
(state) => state.queue.options.includeUnknownSeriesItems,
(app, status, includeUnknownSeriesItems) => {
const {
errors,
warnings,
unknownErrors,
unknownWarnings,
count,
totalCount
unknownCount
} = status.item;
return {
@@ -25,9 +21,7 @@ function createMapStateToProps() {
isReconnecting: app.isReconnecting,
isPopulated: status.isPopulated,
...status.item,
count: includeUnknownSeriesItems ? totalCount : count,
errors: includeUnknownSeriesItems ? errors || unknownErrors : errors,
warnings: includeUnknownSeriesItems ? warnings || unknownWarnings : warnings
count: includeUnknownSeriesItems ? count : count - unknownCount
};
}
);

View File

@@ -1,5 +1,5 @@
.timeleft {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}

View File

@@ -19,7 +19,7 @@ function TimeleftCell(props) {
timeFormat
} = props;
if (status === 'delay') {
if (status === 'Delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
@@ -33,7 +33,7 @@ function TimeleftCell(props) {
);
}
if (status === 'downloadClientUnavailable') {
if (status === 'DownloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
@@ -47,7 +47,7 @@ function TimeleftCell(props) {
);
}
if (!timeleft || status === 'completed' || status === 'failed') {
if (!timeleft) {
return (
<TableRowCell className={styles.timeleft}>
-

View File

@@ -17,7 +17,7 @@
}
.searchInput {
composes: input from '~Components/Form/TextInput.css';
composes: input from 'Components/Form/TextInput.css';
height: 46px;
border-radius: 0;
@@ -35,20 +35,14 @@
.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;

View File

@@ -1,14 +1,13 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import { icons, kinds } from 'Helpers/Props';
import { icons } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import TextInput from 'Components/Form/TextInput';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
import styles from './AddNewSeries.css';
@@ -79,8 +78,7 @@ class AddNewSeries extends Component {
render() {
const {
error,
items,
hasExistingSeries
items
} = this.props;
const term = this.state.term;
@@ -88,7 +86,7 @@ class AddNewSeries extends Component {
return (
<PageContent title="Add New Series">
<PageContentBody>
<PageContentBodyConnector>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon
@@ -102,7 +100,6 @@ class AddNewSeries extends Component {
name="seriesLookup"
value={term}
placeholder="eg. Breaking Bad, tvdb:####"
autoFocus={true}
onChange={this.onSearchInputChange}
/>
@@ -123,13 +120,8 @@ class AddNewSeries extends Component {
}
{
!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
!isFetching && !!error &&
<div>Failed to load search results, please try again.</div>
}
{
@@ -162,36 +154,15 @@ class AddNewSeries extends Component {
}
{
term ?
null :
!term &&
<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 />
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}
@@ -204,7 +175,6 @@ 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
};

View File

@@ -10,15 +10,13 @@ import AddNewSeries from './AddNewSeries';
function createMapStateToProps() {
return createSelector(
(state) => state.addSeries,
(state) => state.series.items.length,
(state) => state.router.location,
(addSeries, existingSeriesCount, location) => {
(state) => state.routing.location,
(addSeries, location) => {
const { params } = parseUrl(location.search);
return {
...addSeries,
term: params.term,
hasExistingSeries: existingSeriesCount > 0
...addSeries
};
}
);

View File

@@ -36,28 +36,28 @@
}
.searchForMissingEpisodesContainer {
composes: container from '~Components/Form/CheckInput.css';
composes: container from 'Components/Form/CheckInput.css';
flex: 0 1 0;
}
.searchForMissingEpisodesInput {
composes: input from '~Components/Form/CheckInput.css';
composes: input from 'Components/Form/CheckInput.css';
margin-top: 0;
}
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
composes: modalFooter from 'Components/Modal/ModalFooter.css';
}
.addButton {
@add-mixin truncate;
composes: button from '~Components/Link/SpinnerButton.css';
composes: button from 'Components/Link/SpinnerButton.css';
}
.hideLanguageProfile {
composes: group from '~Components/Form/FormGroup.css';
composes: group from 'Components/Form/FormGroup.css';
display: none;
}

View File

@@ -14,7 +14,6 @@ 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';
@@ -28,19 +27,10 @@ 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
@@ -57,12 +47,7 @@ class AddNewSeriesModalContent extends Component {
}
onAddSeriesPress = () => {
const {
searchForMissingEpisodes,
seriesType
} = this.state;
this.props.onAddSeriesPress(searchForMissingEpisodes, seriesType);
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
}
//
@@ -81,14 +66,11 @@ class AddNewSeriesModalContent extends Component {
languageProfileId,
seriesType,
seasonFolder,
folder,
tags,
showLanguageProfile,
isSmallScreen,
isWindows,
onModalClose,
onInputChange,
...otherProps
onInputChange
} = this.props;
return (
@@ -105,8 +87,7 @@ class AddNewSeriesModalContent extends Component {
<ModalBody>
<div className={styles.container}>
{
isSmallScreen ?
null :
!isSmallScreen &&
<div className={styles.poster}>
<SeriesPoster
className={styles.poster}
@@ -117,30 +98,17 @@ class AddNewSeriesModalContent extends Component {
}
<div className={styles.info}>
{
overview ?
<div className={styles.overview}>
{overview}
</div> :
null
}
<div className={styles.overview}>
{overview}
</div>
<Form {...otherProps}>
<Form>
<FormGroup>
<FormLabel>Root Folder</FormLabel>
<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}
/>
@@ -215,7 +183,6 @@ class AddNewSeriesModalContent extends Component {
name="seriesType"
onChange={onInputChange}
{...seriesType}
value={this.state.seriesType}
/>
</FormGroup>
@@ -278,7 +245,6 @@ 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,
@@ -288,11 +254,9 @@ 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

View File

@@ -4,7 +4,6 @@ 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';
@@ -13,8 +12,7 @@ function createMapStateToProps() {
(state) => state.addSeries,
(state) => state.settings.languageProfiles,
createDimensionsSelector(),
createSystemStatusSelector(),
(addSeriesState, languageProfiles, dimensions, systemStatus) => {
(addSeriesState, languageProfiles, dimensions) => {
const {
isAdding,
addError,
@@ -34,7 +32,6 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen,
validationErrors,
validationWarnings,
isWindows: systemStatus.isWindows,
...settings
};
}
@@ -55,13 +52,14 @@ class AddNewSeriesModalContentConnector extends Component {
this.props.setAddSeriesDefault({ [name]: value });
}
onAddSeriesPress = (searchForMissingEpisodes, seriesType) => {
onAddSeriesPress = (searchForMissingEpisodes) => {
const {
tvdbId,
rootFolderPath,
monitor,
qualityProfileId,
languageProfileId,
seriesType,
seasonFolder,
tags
} = this.props;
@@ -72,7 +70,7 @@ class AddNewSeriesModalContentConnector extends Component {
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
languageProfileId: languageProfileId.value,
seriesType,
seriesType: seriesType.value,
seasonFolder: seasonFolder.value,
tags: tags.value,
searchForMissingEpisodes

View File

@@ -1,15 +1,10 @@
.searchResult {
position: relative;
display: flex;
margin: 20px 0;
padding: 20px;
width: 100%;
color: inherit;
}
.underlay {
@add-mixin cover;
background-color: $white;
color: inherit;
transition: background 500ms;
&:hover {
@@ -19,25 +14,13 @@
}
}
.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;
}
@@ -47,22 +30,9 @@
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;
pointer-events: all;
}
.overview {

View File

@@ -24,7 +24,7 @@ class AddNewSeriesSearchResult extends Component {
componentDidUpdate(prevProps) {
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
this.onAddSeriesModalClose();
this.onAddSerisModalClose();
}
}
@@ -35,14 +35,10 @@ class AddNewSeriesSearchResult extends Component {
this.setState({ isNewAddSeriesModalOpen: true });
}
onAddSeriesModalClose = () => {
onAddSerisModalClose = () => {
this.setState({ isNewAddSeriesModalOpen: false });
}
onTVDBLinkPress = (event) => {
event.stopPropagation();
}
//
// Render
@@ -57,8 +53,6 @@ class AddNewSeriesSearchResult extends Component {
overview,
statistics,
ratings,
folder,
seriesType,
images,
isExistingSeries,
isSmallScreen
@@ -78,58 +72,38 @@ class AddNewSeriesSearchResult extends Component {
}
return (
<div className={styles.searchResult}>
<div>
<Link
className={styles.underlay}
className={styles.searchResult}
{...linkProps}
/>
<div className={styles.overlay}>
>
{
isSmallScreen ?
null :
<SeriesPoster
className={styles.poster}
images={images}
size={250}
overflow={true}
/>
!isSmallScreen &&
<SeriesPoster
className={styles.poster}
images={images}
size={250}
/>
}
<div className={styles.content}>
<div>
<div className={styles.title}>
{title}
{
!title.contains(year) && year ?
<span className={styles.year}>
({year})
</span> :
null
!title.contains(year) && !!year &&
<span className={styles.year}>({year})</span>
}
{
isExistingSeries ?
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/> :
null
}
<Link
className={styles.tvdbLink}
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
onPress={this.onTVDBLinkPress}
>
isExistingSeries &&
<Icon
className={styles.tvdbLinkIcon}
name={icons.EXTERNAL_LINK}
size={28}
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/>
</Link>
}
</div>
<div>
@@ -141,41 +115,27 @@ class AddNewSeriesSearchResult extends Component {
</Label>
{
network ?
<Label size={sizes.LARGE}>
{network}
</Label> :
null
!!network &&
<Label size={sizes.LARGE}>
{network}
</Label>
}
{
seasonCount ?
<Label size={sizes.LARGE}>
{seasons}
</Label> :
null
!!seasonCount &&
<Label size={sizes.LARGE}>
{seasons}
</Label>
}
{
status === 'ended' ?
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
status === 'ended' &&
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
Ended
</Label> :
null
}
{
status === 'upcoming' ?
<Label
kind={kinds.INFO}
size={sizes.LARGE}
>
Upcoming
</Label> :
null
</Label>
}
</div>
@@ -183,7 +143,7 @@ class AddNewSeriesSearchResult extends Component {
{overview}
</div>
</div>
</div>
</Link>
<AddNewSeriesModal
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
@@ -191,10 +151,8 @@ class AddNewSeriesSearchResult extends Component {
title={title}
year={year}
overview={overview}
folder={folder}
initialSeriesType={seriesType}
images={images}
onModalClose={this.onAddSeriesModalClose}
onModalClose={this.onAddSerisModalClose}
/>
</div>
);
@@ -211,8 +169,6 @@ 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

View File

@@ -5,7 +5,7 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
@@ -21,15 +21,17 @@ class ImportSeries extends Component {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
selectedState: {},
contentBody: null,
scrollTop: 0
};
}
//
// Control
setScrollerRef = (ref) => {
this.setState({ scroller: ref });
setContentBodyRef = (ref) => {
this.setState({ contentBody: ref });
}
//
@@ -92,13 +94,13 @@ class ImportSeries extends Component {
allSelected,
allUnselected,
selectedState,
scroller
contentBody
} = this.state;
return (
<PageContent title="Import Series">
<PageContentBody
registerScroller={this.setScrollerRef}
<PageContentBodyConnector
ref={this.setContentBodyRef}
onScroll={this.onScroll}
>
{
@@ -119,21 +121,23 @@ class ImportSeries extends Component {
}
{
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
<ImportSeriesTableConnector
rootFolderId={rootFolderId}
unmappedFolders={unmappedFolders}
allSelected={allSelected}
allUnselected={allUnselected}
selectedState={selectedState}
scroller={scroller}
contentBody={contentBody}
showLanguageProfile={showLanguageProfile}
scrollTop={this.state.scrollTop}
onSelectAllChange={this.onSelectAllChange}
onSelectedChange={this.onSelectedChange}
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
onScroll={this.onScroll}
/>
}
</PageContentBody>
</PageContentBodyConnector>
{
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&

View File

@@ -14,7 +14,7 @@
}
.importButton {
composes: button from '~Components/Link/SpinnerButton.css';
composes: button from 'Components/Link/SpinnerButton.css';
height: 35px;
}
@@ -26,7 +26,7 @@
}
.loading {
composes: loading from '~Components/Loading/LoadingIndicator.css';
composes: loading from 'Components/Loading/LoadingIndicator.css';
margin: 0 10px 0 12px;
text-align: left;

View File

@@ -1,11 +1,11 @@
.folder {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 1 0 200px;
}
.monitor {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 200px;
min-width: 185px;
@@ -13,28 +13,28 @@
.qualityProfile,
.languageProfile {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 250px;
min-width: 170px;
}
.seriesType {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 200px;
min-width: 120px;
}
.seasonFolder {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 150px;
min-width: 120px;
}
.series {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 400px;
min-width: 300px;

View File

@@ -1,16 +1,16 @@
.selectInput {
composes: input from '~Components/Form/CheckInput.css';
composes: input from 'Components/Form/CheckInput.css';
}
.folder {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 1 0 200px;
line-height: 36px;
}
.monitor {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 200px;
min-width: 185px;
@@ -18,35 +18,35 @@
.qualityProfile,
.languageProfile {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 250px;
min-width: 170px;
}
.seriesType {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 200px;
min-width: 120px;
}
.seasonFolder {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 150px;
min-width: 120px;
}
.series {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 400px;
min-width: 300px;
}
.hideLanguageProfile {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
display: none;
}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes } from 'Helpers/Props';
import FormInputGroup from 'Components/Form/FormInputGroup';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
@@ -9,6 +10,7 @@ import styles from './ImportSeriesRow.css';
function ImportSeriesRow(props) {
const {
style,
id,
monitor,
qualityProfileId,
@@ -24,7 +26,7 @@ function ImportSeriesRow(props) {
} = props;
return (
<>
<VirtualTableRow style={style}>
<VirtualTableSelectCell
inputClassName={styles.selectInput}
id={id}
@@ -88,14 +90,14 @@ function ImportSeriesRow(props) {
<ImportSeriesSelectSeriesConnector
id={id}
isExistingSeries={isExistingSeries}
onInputChange={onInputChange}
/>
</VirtualTableRowCell>
</>
</VirtualTableRow>
);
}
ImportSeriesRow.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,

View File

@@ -1,3 +1,3 @@
.input {
composes: input from '~Components/Form/CheckInput.css';
composes: input from 'Components/Form/CheckInput.css';
}

View File

@@ -2,7 +2,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import ImportSeriesHeader from './ImportSeriesHeader';
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
@@ -113,19 +112,15 @@ class ImportSeriesTable extends Component {
const item = items[rowIndex];
return (
<VirtualTableRow
<ImportSeriesRowConnector
key={key}
style={style}
>
<ImportSeriesRowConnector
key={item.id}
rootFolderId={rootFolderId}
showLanguageProfile={showLanguageProfile}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
</VirtualTableRow>
rootFolderId={rootFolderId}
showLanguageProfile={showLanguageProfile}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
);
}
@@ -138,10 +133,12 @@ class ImportSeriesTable extends Component {
allSelected,
allUnselected,
isSmallScreen,
scroller,
contentBody,
showLanguageProfile,
scrollTop,
selectedState,
onSelectAllChange
onSelectAllChange,
onScroll
} = this.props;
if (!items.length) {
@@ -151,9 +148,10 @@ class ImportSeriesTable extends Component {
return (
<VirtualTable
items={items}
scroller={scroller}
contentBody={contentBody}
isSmallScreen={isSmallScreen}
rowHeight={52}
scrollTop={scrollTop}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
@@ -165,6 +163,7 @@ class ImportSeriesTable extends Component {
/>
}
selectedState={selectedState}
onScroll={onScroll}
/>
);
}
@@ -184,13 +183,15 @@ ImportSeriesTable.propTypes = {
selectedState: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
allSeries: PropTypes.arrayOf(PropTypes.object),
scroller: PropTypes.instanceOf(Element).isRequired,
contentBody: PropTypes.object.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,
scrollTop: PropTypes.number.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemoveSelectedStateItem: PropTypes.func.isRequired,
onSeriesLookup: PropTypes.func.isRequired,
onSetImportSeriesValue: PropTypes.func.isRequired
onSetImportSeriesValue: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
};
export default ImportSeriesTable;

View File

@@ -1,6 +1,11 @@
.button {
composes: link from '~Components/Link/Link.css';
.tether {
z-index: 2000;
}
.button {
composes: link from 'Components/Link/Link.css';
position: relative;
display: flex;
align-items: center;
padding: 6px 16px;
@@ -31,10 +36,9 @@
}
.contentContainer {
z-index: $popperZIndex;
margin-top: 4px;
/* 400px container witdh with 8px padding on each side */
width: 384px;
padding: 0 8px;
width: 400px;
}
.content {
@@ -61,7 +65,7 @@
}
.searchInput {
composes: input from '~Components/Form/TextInput.css';
composes: input from 'Components/Form/TextInput.css';
border-radius: 0;
}

View File

@@ -1,10 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import ReactDOM from 'react-dom';
import TetherComponent from 'react-tether';
import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Portal from 'Components/Portal';
import FormInputButton from 'Components/Form/FormInputButton';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -13,6 +12,19 @@ import ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnect
import ImportSeriesTitle from './ImportSeriesTitle';
import styles from './ImportSeriesSelectSeries.css';
const tetherOptions = {
skipMoveElement: true,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
}
],
attachment: 'top center',
targetAttachment: 'bottom center'
};
class ImportSeriesSelectSeries extends Component {
//
@@ -22,9 +34,6 @@ class ImportSeriesSelectSeries extends Component {
super(props, context);
this._seriesLookupTimeout = null;
this._scheduleUpdate = null;
this._buttonId = getUniqueElememtId();
this._contentId = getUniqueElememtId();
this.state = {
term: props.id,
@@ -32,15 +41,17 @@ class ImportSeriesSelectSeries extends Component {
};
}
componentDidUpdate() {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
}
//
// Control
_setButtonRef = (ref) => {
this._buttonRef = ref;
}
_setContentRef = (ref) => {
this._contentRef = ref;
}
_addListener() {
window.addEventListener('click', this.onWindowClick);
}
@@ -53,18 +64,14 @@ class ImportSeriesSelectSeries extends Component {
// Listeners
onWindowClick = (event) => {
const button = document.getElementById(this._buttonId);
const content = document.getElementById(this._contentId);
const button = ReactDOM.findDOMNode(this._buttonRef);
const content = ReactDOM.findDOMNode(this._contentRef);
if (!button || !content) {
if (!button) {
return;
}
if (
!button.contains(event.target) &&
!content.contains(event.target) &&
this.state.isOpen
) {
if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) {
this.setState({ isOpen: false });
this._removeListener();
}
@@ -122,158 +129,129 @@ class ImportSeriesSelectSeries extends Component {
error.responseJSON.message;
return (
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._buttonId}
>
<Link
ref={ref}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpSeries && isQueued && !isPopulated ?
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
}
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions}
>
<Link
ref={this._setButtonRef}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpSeries && isQueued && !isPopulated &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
isPopulated && selectedSeries && isExistingSeries ?
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/> :
null
}
{
isPopulated && selectedSeries && isExistingSeries &&
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
}
{
isPopulated && selectedSeries ?
<ImportSeriesTitle
title={selectedSeries.title}
year={selectedSeries.year}
network={selectedSeries.network}
isExistingSeries={isExistingSeries}
/> :
null
}
{
isPopulated && selectedSeries &&
<ImportSeriesTitle
title={selectedSeries.title}
year={selectedSeries.year}
network={selectedSeries.network}
isExistingSeries={isExistingSeries}
/>
}
{
isPopulated && !selectedSeries ?
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
{
isPopulated && !selectedSeries &&
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
No match found!
</div> :
null
}
</div>
}
{
!isFetching && !!error ?
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
{
!isFetching && !!error &&
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
Search failed, please try again later.
</div> :
null
}
</div>
}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
{
this.state.isOpen &&
<div
ref={this._setContentRef}
className={styles.contentContainer}
>
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
</Link>
</div>
)}
</Reference>
<Portal>
<Popper
placement="bottom"
modifiers={{
preventOverflow: {
boundariesElement: 'viewport'
}
}}
>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
ref={ref}
id={this._contentId}
className={styles.contentContainer}
style={style}
>
<div className={styles.results}>
{
this.state.isOpen ?
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
<div className={styles.results}>
{
items.map((item) => {
return (
<ImportSeriesSearchResultConnector
key={item.tvdbId}
tvdbId={item.tvdbId}
title={item.title}
year={item.year}
network={item.network}
onPress={this.onSeriesSelect}
/>
);
})
}
</div>
</div> :
null
items.map((item) => {
return (
<ImportSeriesSearchResultConnector
key={item.tvdbId}
tvdbId={item.tvdbId}
title={item.title}
year={item.year}
network={item.network}
onPress={this.onSeriesSelect}
/>
);
})
}
</div>
);
}}
</Popper>
</Portal>
</Manager>
</div>
</div>
}
</TetherComponent>
);
}
}

View File

@@ -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,23 +41,13 @@ class ImportSeriesSelectSeriesConnector extends Component {
onSeriesSelect = (tvdbId) => {
const {
id,
items,
onInputChange
items
} = this.props;
const selectedSeries = items.find((item) => item.tvdbId === tvdbId);
this.props.setImportSeriesValue({
id,
selectedSeries
selectedSeries: _.find(items, { tvdbId })
});
if (selectedSeries.seriesType !== seriesTypes.STANDARD) {
onInputChange({
name: 'seriesType',
value: selectedSeries.seriesType
});
}
}
//
@@ -79,7 +69,6 @@ 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
};

View File

@@ -0,0 +1,18 @@
.link {
composes: link from 'Components/Link/Link.css';
display: block;
}
.freeSpace,
.unmappedFolders {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 45px;
}

View File

@@ -1,57 +1,41 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
import { icons, kinds } from 'Helpers/Props';
import Label from 'Components/Label';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import styles from './RootFolderRow.css';
import styles from './ImportSeriesRootFolderRow.css';
function RootFolderRow(props) {
function ImportSeriesRootFolderRow(props) {
const {
id,
path,
accessible,
freeSpace,
unmappedFolders,
onDeletePress
} = props;
const isUnavailable = !accessible;
const unmappedFoldersCount = unmappedFolders.length || '-';
return (
<TableRow>
<TableRowCell>
{
isUnavailable ?
<div className={styles.unavailablePath}>
{path}
<Label
className={styles.unavailableLabel}
kind={kinds.DANGER}
>
Unavailable
</Label>
</div> :
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
}
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
</TableRowCell>
<TableRowCell className={styles.freeSpace}>
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
{formatBytes(freeSpace) || '-'}
</TableRowCell>
<TableRowCell className={styles.unmappedFolders}>
{isUnavailable ? '-' : unmappedFolders.length}
{unmappedFoldersCount}
</TableRowCell>
<TableRowCell className={styles.actions}>
@@ -65,17 +49,17 @@ function RootFolderRow(props) {
);
}
RootFolderRow.propTypes = {
ImportSeriesRootFolderRow.propTypes = {
id: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
accessible: PropTypes.bool.isRequired,
freeSpace: PropTypes.number,
freeSpace: PropTypes.number.isRequired,
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired
};
RootFolderRow.defaultProps = {
ImportSeriesRootFolderRow.defaultProps = {
freeSpace: 0,
unmappedFolders: []
};
export default RootFolderRow;
export default ImportSeriesRootFolderRow;

View File

@@ -0,0 +1,48 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
import ImportSeriesRootFolderRow from './ImportSeriesRootFolderRow';
function createMapStateToProps() {
return createSelector(
() => {
return {
};
}
);
}
const mapDispatchToProps = {
deleteRootFolder
};
class ImportSeriesRootFolderRowConnector extends Component {
//
// Listeners
onDeletePress = () => {
this.props.deleteRootFolder({ id: this.props.id });
}
//
// Render
render() {
return (
<ImportSeriesRootFolderRow
{...this.props}
onDeletePress={this.onDeletePress}
/>
);
}
}
ImportSeriesRootFolderRowConnector.propTypes = {
id: PropTypes.number.isRequired,
deleteRootFolder: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRootFolderRowConnector);

View File

@@ -2,15 +2,39 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import RootFolders from 'RootFolder/RootFolders';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import ImportSeriesRootFolderRowConnector from './ImportSeriesRootFolderRowConnector';
import styles from './ImportSeriesSelectFolder.css';
const rootFolderColumns = [
{
name: 'path',
label: 'Path',
isVisible: true
},
{
name: 'freeSpace',
label: 'Free Space',
isVisible: true
},
{
name: 'unmappedFolders',
label: 'Unmapped Folders',
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class ImportSeriesSelectFolder extends Component {
//
@@ -53,7 +77,7 @@ class ImportSeriesSelectFolder extends Component {
return (
<PageContent title="Import Series">
<PageContentBody>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -75,10 +99,10 @@ class ImportSeriesSelectFolder extends Component {
Some tips to ensure the import goes smoothly:
<ul>
<li className={styles.tip}>
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
Make sure your files include the quality in the name. 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>
@@ -86,13 +110,26 @@ class ImportSeriesSelectFolder extends Component {
{
items.length > 0 ?
<div className={styles.recentFolders}>
<FieldSet legend="Root Folders">
<RootFolders
isFetching={isFetching}
isPopulated={isPopulated}
error={error}
items={items}
/>
<FieldSet legend="Recent Folders">
<Table
columns={rootFolderColumns}
>
<TableBody>
{
items.map((rootFolder) => {
return (
<ImportSeriesRootFolderRowConnector
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})
}
</TableBody>
</Table>
</FieldSet>
<Button
@@ -132,7 +169,7 @@ class ImportSeriesSelectFolder extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}
@@ -144,7 +181,8 @@ ImportSeriesSelectFolder.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired
onNewRootFolderSelect: PropTypes.func.isRequired,
onDeleteRootFolderPress: PropTypes.func.isRequired
};
export default ImportSeriesSelectFolder;

View File

@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { push } from 'connected-react-router';
import { push } from 'react-router-redux';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import { fetchRootFolders, addRootFolder } from 'Store/Actions/rootFolderActions';
import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions';
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
function createMapStateToProps() {
@@ -24,6 +24,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
fetchRootFolders,
addRootFolder,
deleteRootFolder,
push
};
@@ -59,6 +60,10 @@ class ImportSeriesSelectFolderConnector extends Component {
this.props.addRootFolder({ path });
}
onDeleteRootFolderPress = (id) => {
this.props.deleteRootFolder({ id });
}
//
// Render
@@ -67,6 +72,7 @@ class ImportSeriesSelectFolderConnector extends Component {
<ImportSeriesSelectFolder
{...this.props}
onNewRootFolderSelect={this.onNewRootFolderSelect}
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
/>
);
}
@@ -78,6 +84,7 @@ ImportSeriesSelectFolderConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchRootFolders: PropTypes.func.isRequired,
addRootFolder: PropTypes.func.isRequired,
deleteRootFolder: PropTypes.func.isRequired,
push: PropTypes.func.isRequired
};

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import { ConnectedRouter } from 'react-router-redux';
import PageConnector from 'Components/Page/PageConnector';
import AppRoutes from './AppRoutes';

View File

@@ -33,7 +33,6 @@ import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Diagnostic from 'Diagnostic/Diagnostic';
function AppRoutes(props) {
const {
@@ -230,15 +229,6 @@ function AppRoutes(props) {
component={Logs}
/>
{/*
Diagnostics
*/}
<Route
path="/diag"
component={Diagnostic}
/>
{/*
Not Found
*/}

View File

@@ -1,6 +0,0 @@
import React from 'react';
const ColorImpairedContext = React.createContext(false);
export const ColorImpairedConsumer = ColorImpairedContext.Consumer;
export default ColorImpairedContext;

View File

@@ -63,27 +63,27 @@
*/
.downloaded {
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
composes: downloaded from 'Calendar/Events/CalendarEvent.css';
}
.downloading {
composes: downloading from '~Calendar/Events/CalendarEvent.css';
composes: downloading from 'Calendar/Events/CalendarEvent.css';
}
.unmonitored {
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
composes: unmonitored from 'Calendar/Events/CalendarEvent.css';
}
.onAir {
composes: onAir from '~Calendar/Events/CalendarEvent.css';
composes: onAir from 'Calendar/Events/CalendarEvent.css';
}
.missing {
composes: missing from '~Calendar/Events/CalendarEvent.css';
composes: missing from 'Calendar/Events/CalendarEvent.css';
}
.premiere {
composes: premiere from '~Calendar/Events/CalendarEvent.css';
composes: premiere from 'Calendar/Events/CalendarEvent.css';
}
@media only screen and (max-width: $breakpointSmall) {

View File

@@ -175,7 +175,6 @@ class CalendarConnector extends Component {
}
CalendarConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
time: PropTypes.string,
view: PropTypes.string.isRequired,
firstDayOfWeek: PropTypes.number.isRequired,

View File

@@ -1,11 +1,11 @@
.calendarPageBody {
composes: contentBody from '~Components/Page/PageContentBody.css';
composes: contentBody from 'Components/Page/PageContentBody.css';
display: flex;
}
.calendarInnerPageBody {
composes: innerContentBody from '~Components/Page/PageContentBody.css';
composes: innerContentBody from 'Components/Page/PageContentBody.css';
display: flex;
flex-direction: column;

View File

@@ -3,10 +3,9 @@ import React, { Component } from 'react';
import { align, icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
import Measure from 'Components/Measure';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import NoSeries from 'Series/NoSeries';
@@ -77,10 +76,8 @@ class CalendarPage extends Component {
filters,
hasSeries,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing,
useCurrentPage,
onRssSyncPress,
onFilterSelect
} = this.props;
@@ -102,15 +99,6 @@ class CalendarPage extends Component {
onPress={this.onGetCalendarLinkPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="RSS Sync"
iconName={icons.RSS}
isSpinning={isRssSyncExecuting}
onPress={onRssSyncPress}
/>
<PageToolbarButton
label="Search for Missing"
iconName={icons.SEARCH}
@@ -138,7 +126,7 @@ class CalendarPage extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody
<PageContentBodyConnector
className={styles.calendarPageBody}
innerClassName={styles.calendarInnerPageBody}
>
@@ -159,7 +147,7 @@ class CalendarPage extends Component {
hasSeries &&
<LegendConnector />
}
</PageContentBody>
</PageContentBodyConnector>
<CalendarLinkModal
isOpen={isCalendarLinkModalOpen}
@@ -180,12 +168,10 @@ CalendarPage.propTypes = {
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasSeries: PropTypes.bool.isRequired,
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
onSearchMissingPress: PropTypes.func.isRequired,
onDaysCountChange: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};

View File

@@ -3,14 +3,11 @@ import { createSelector } from 'reselect';
import moment from 'moment';
import { isCommandExecuting } from 'Utilities/Command';
import isBefore from 'Utilities/Date/isBefore';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import CalendarPage from './CalendarPage';
function createMissingEpisodeIdsSelector() {
@@ -28,7 +25,7 @@ function createMissingEpisodeIdsSelector() {
moment(airDateUtc).isAfter(start) &&
moment(airDateUtc).isBefore(end) &&
isBefore(episode.airDateUtc) &&
!queueDetails.some((details) => !!details.episode && details.episode.id === episode.id)
!queueDetails.some((details) => details.episode.id === episode.id)
) {
acc.push(episode.id);
}
@@ -62,7 +59,6 @@ function createMapStateToProps() {
createSeriesCountSelector(),
createUISettingsSelector(),
createMissingEpisodeIdsSelector(),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createIsSearchingSelector(),
(
selectedFilterKey,
@@ -70,7 +66,6 @@ function createMapStateToProps() {
seriesCount,
uiSettings,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing
) => {
return {
@@ -79,7 +74,6 @@ function createMapStateToProps() {
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasSeries: !!seriesCount,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing
};
}
@@ -88,12 +82,6 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRssSyncPress() {
dispatch(executeCommand({
name: commandNames.RSS_SYNC
}));
},
onSearchMissingPress(episodeIds) {
dispatch(searchMissing({ episodeIds }));
},

View File

@@ -2,8 +2,8 @@
flex: 1 0 14.28%;
overflow: hidden;
min-height: 70px;
border-bottom: 1px solid $calendarBorderColor;
border-left: 1px solid $calendarBorderColor;
border-bottom: 1px solid $borderColor;
border-left: 1px solid $borderColor;
}
.isSingleDay {
@@ -12,7 +12,7 @@
.dayOfMonth {
padding-right: 5px;
border-bottom: 1px solid $calendarBorderColor;
border-bottom: 1px solid $borderColor;
text-align: right;
}

View File

@@ -1,6 +1,6 @@
.days {
display: flex;
border-right: 1px solid $calendarBorderColor;
border-right: 1px solid $borderColor;
}
.day,

View File

@@ -5,10 +5,6 @@
border-bottom: 1px solid $borderColor;
border-left: 4px solid $borderColor;
font-size: 12px;
&:global(.colorImpaired) {
border-left-width: 5px;
}
}
.info,
@@ -16,13 +12,10 @@
display: flex;
}
.episodeInfo {
color: $calendarTextDim;
}
.seriesTitle,
.episodeTitle {
@add-mixin truncate;
flex: 1 0 1px;
margin-right: 10px;
}
@@ -40,20 +33,12 @@
margin-left: 3px;
}
.airTime {
color: $calendarTextDim;
}
/*
* Status
*/
.downloaded {
border-left-color: $successColor !important;
&:global(.colorImpaired) {
border-left-color: color($successColor, saturation(+15%)) !important;
}
}
.downloading {
@@ -64,7 +49,7 @@
border-left-color: $gray !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
background: repeating-linear-gradient(45deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -72,7 +57,7 @@
border-left-color: $warningColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -80,8 +65,7 @@
border-left-color: $dangerColor !important;
&:global(.colorImpaired) {
border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -89,6 +73,6 @@
border-left-color: $primaryColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}

Some files were not shown because too many files have changed in this diff Show More