1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-13 15:34:28 -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
1222 changed files with 33958 additions and 24702 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

@@ -20,39 +20,33 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
### Requirements
* [Visual Studio 2017](https://www.visualstudio.com/vs/)
* Visual Studio 2015 (https://www.visualstudio.com/vs/)
* [Git](https://git-scm.com/downloads)
* [NodeJS](https://nodejs.org/en/download/)
* [Yarn](https://yarnpkg.com/)
### Setup
* Make sure all the required software mentioned above are installed
* Clone the repository into your development machine. [*info*](https://help.github.com/en/articles/working-with-forks)
* 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 `yarn`
* 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
### Development
* Open `NzbDrone.sln` in Visual Studio
* Make sure `NzbDrone.Console` is set as the startup project
* Build `NzbDrone.Windows` and `NzbDrone.Mono` projects
* Build Solution
### UI Development
* Run `yarn watch` to build UI and rebuild automatically when changes are detected
* Run Sonarr.Console.exe (or debug in Visual Studio)
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2019
* 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/)
* [WebStorm](http://www.jetbrains.com/webstorm/)
* [TeamCity](http://www.jetbrains.com/teamcity/)

186
build.sh
View File

@@ -1,18 +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
sqlitePackageDir="$HOME/.nuget/packages/system.data.sqlite.core.lidarr/1.0.111-5"
updateFolder=$outputFolder/Sonarr.Update
updateFolderMono=$outputFolderLinux/Sonarr.Update
nuget='tools/nuget/nuget.exe';
vswhere='tools/vswhere/vswhere.exe';
@@ -50,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
}
@@ -90,17 +86,16 @@ 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
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x64 //t:Clean //m
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=x64 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
}
BuildWithXbuild()
@@ -108,13 +103,13 @@ BuildWithXbuild()
export MONO_IOMAP=case
CheckExitCode xbuild /t:Clean $slnFile
mono $nuget restore $slnFile
CheckExitCode xbuild /p:Configuration=Release /p:Platform=x64 /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'
@@ -127,7 +122,6 @@ Build()
ProgressStart 'Build'
rm -rf $outputFolder
rm -rf $testPackageFolder
if [ $runtime = "dotnet" ] ; then
BuildWithMSBuild
@@ -137,6 +131,9 @@ Build()
CleanFolder $outputFolder false
echo "Removing Mono.Posix.dll"
rm $outputFolder/Mono.Posix.dll
ProgressEnd 'Build'
}
@@ -170,36 +167,6 @@ CreateMdbs()
fi
}
PatchMono()
{
local path=$1
# Copy over the netstandard.dll facade since mono has no separate package for it and includes it in mono-devel
for assembly in netstandard System.Runtime
do
echo "Copy Mono-specific facade $assembly.dll"
cp $sourceFolder/Libraries/Mono/$assembly.dll $path/$assembly.dll
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'
@@ -223,11 +190,16 @@ PackageMono()
rm -f $outputFolderLinux/sqlite3.*
rm -f $outputFolderLinux/MediaInfo.*
PatchMono $outputFolderLinux
echo "Adding Sonarr.Core.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $outputFolderLinux
echo "Adding CurlSharp.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderLinux
echo "Adding unix System.Runtime.InteropServices.RuntimeInformation.dll (for SharpRaven)"
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux/Sonarr.Update
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
rm $outputFolderLinux/Sonarr.exe*
for file in $outputFolderLinux/Sonarr.Console.exe*; do
@@ -238,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'
}
@@ -257,11 +229,11 @@ PackageMacOS()
echo "Copying Binaries"
cp -r $outputFolderLinux/* $outputFolderMacOS
echo "Adding sqlite dylib"
cp "$sqlitePackageDir/runtimes/osx-x64/native/net46"/* $outputFolderMacOS
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
echo "Adding MediaInfo dylib"
cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOS
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOS
ProgressEnd 'Creating MacOS Package'
}
@@ -282,11 +254,11 @@ PackageMacOSApp()
echo "Copying Binaries"
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
echo "Adding sqlite dylib"
cp "$sqlitePackageDir/runtimes/osx-x64/native/net46"/* $outputFolderMacOS
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
echo "Adding MediaInfo dylib"
cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOS
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
echo "Removing Update Folder"
rm -r $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr.Update
@@ -294,88 +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
cp ./test.sh $testPackageFolderLinux/
dos2unix $testPackageFolderLinux/test.sh
echo "Adding CurlSharp.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder
echo "Removing Sonarr.Windows"
rm $testPackageFolderLinux/Sonarr.Windows.*
echo "Copying CurlSharp libraries"
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
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()
@@ -383,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']"
@@ -417,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

@@ -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
# 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
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.8=preview-xenial"
# Semi-Supported versions
MONO_VERSIONS="$MONO_VERSIONS 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,18 +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',
'copyJs'
)
)
);
gulp.task('build', () => {
return runSequence('clean', [
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
]);
});

View File

@@ -1,15 +1,15 @@
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')
], { base: paths.src.root })
])
.pipe(cache('copyJs'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
@@ -17,7 +17,7 @@ gulp.task('copyJs', () => {
});
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))
@@ -26,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,62 +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 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 distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('ROOT:', root);
console.log('isProduction:', isProduction);
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: {
@@ -71,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: {
@@ -86,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: {
@@ -116,57 +101,53 @@ const config = {
{
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
@@ -214,16 +195,17 @@ const config = {
};
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

@@ -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,10 +1,9 @@
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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
@@ -42,18 +41,6 @@ class Blacklist extends Component {
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>

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,16 +231,6 @@ function HistoryDetails(props) {
</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

@@ -5,7 +5,6 @@ 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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
@@ -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}

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

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

@@ -3,10 +3,9 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
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';
@@ -17,7 +16,6 @@ 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';
@@ -45,27 +43,22 @@ class Queue extends Component {
// before episodes start fetching or when episodes start fetching.
if (
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items) &&
nextProps.items.some((e) => e.episodeId)
(
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items)
) ||
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
) {
return false;
}
if (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
});
this.setState({ selectedState: {} });
return;
}
@@ -146,7 +139,7 @@ class Queue extends Component {
} = this.state;
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
const hasError = error || episodesError;
const selectedCount = this.getSelectedIds().length;
const disableSelectedActions = selectedCount === 0;
@@ -180,21 +173,6 @@ 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>
<PageContentBodyConnector>

View File

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

@@ -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';
@@ -72,11 +71,9 @@ class QueueRow extends Component {
errorMessage,
series,
episode,
language,
quality,
protocol,
indexer,
outputPath,
downloadClient,
estimatedCompletionTime,
timeleft,
@@ -207,16 +204,6 @@ class QueueRow extends Component {
);
}
if (name === 'language') {
return (
<TableRowCell key={name}>
<EpisodeLanguage
language={language}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
@@ -253,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
@@ -369,11 +340,9 @@ QueueRow.propTypes = {
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,5 +1,5 @@
.status {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 30px;
}

View File

@@ -116,7 +116,6 @@ function QueueStatusCell(props) {
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={tooltipPositions.RIGHT}
canFlip={false}
/>
</TableRowCell>
);

View File

@@ -1,4 +1,3 @@
.messageRemove {
.message {
margin-bottom: 30px;
color: $dangerColor;
}

View File

@@ -69,14 +69,10 @@ class RemoveQueueItemModal extends Component {
</ModalHeader>
<ModalBody>
<div>
<div className={styles.message}>
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
<div className={styles.messageRemove}>
Removing will remove the download and the file(s) from the download client.
</div>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup

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

@@ -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,7 +1,6 @@
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';
@@ -79,8 +78,7 @@ class AddNewSeries extends Component {
render() {
const {
error,
items,
hasExistingSeries
items
} = this.props;
const term = this.state.term;
@@ -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,34 +154,13 @@ 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 />
</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

@@ -66,14 +66,11 @@ class AddNewSeriesModalContent extends Component {
languageProfileId,
seriesType,
seasonFolder,
folder,
tags,
showLanguageProfile,
isSmallScreen,
isWindows,
onModalClose,
onInputChange,
...otherProps
onInputChange
} = this.props;
return (
@@ -90,8 +87,7 @@ class AddNewSeriesModalContent extends Component {
<ModalBody>
<div className={styles.container}>
{
isSmallScreen ?
null :
!isSmallScreen &&
<div className={styles.poster}>
<SeriesPoster
className={styles.poster}
@@ -102,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}
/>
@@ -271,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
};
}

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,18 +30,6 @@
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;

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,7 +53,6 @@ class AddNewSeriesSearchResult extends Component {
overview,
statistics,
ratings,
folder,
images,
isExistingSeries,
isSmallScreen
@@ -77,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>
@@ -140,30 +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
</Label>
}
</div>
@@ -171,7 +143,7 @@ class AddNewSeriesSearchResult extends Component {
{overview}
</div>
</div>
</div>
</Link>
<AddNewSeriesModal
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
@@ -179,9 +151,8 @@ class AddNewSeriesSearchResult extends Component {
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddSeriesModalClose}
onModalClose={this.onAddSerisModalClose}
/>
</div>
);
@@ -198,7 +169,6 @@ AddNewSeriesSearchResult.propTypes = {
overview: PropTypes.string,
statistics: PropTypes.object.isRequired,
ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingSeries: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired

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

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

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

@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import RootFolders from 'RootFolder/RootFolders';
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 {
//
@@ -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
@@ -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

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

@@ -25,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);
}

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);
}
}

View File

@@ -200,7 +200,7 @@ class CalendarEvent extends Component {
</div>
}
<div className={styles.airTime}>
<div>
{formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
</div>
</Link>

View File

@@ -14,6 +14,7 @@
.seriesTitle {
@add-mixin truncate;
flex: 1 0 1px;
margin-right: 10px;
color: #3a3f51;
@@ -22,12 +23,10 @@
.airTime {
flex: 1 0 1px;
color: $calendarTextDim;
}
.episodeInfo {
margin-left: 10px;
color: $calendarTextDim;
}
.absoluteEpisodeNumber {
@@ -59,29 +58,25 @@
*/
.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';
}
.unaired {
composes: unaired from '~Calendar/Events/CalendarEvent.css';
composes: premiere from 'Calendar/Events/CalendarEvent.css';
}

View File

@@ -10,7 +10,7 @@ function createIsDownloadingSelector() {
(state) => state.queue.details,
(episodeIds, details) => {
return details.items.some((item) => {
return item.episode && episodeIds.includes(item.episode.id);
return episodeIds.includes(item.episode.id);
});
}
);

View File

@@ -8,7 +8,7 @@
}
.todayButton {
composes: button from '~Components/Link/Button.css';
composes: button from 'Components/Link/Button.css';
margin-left: 5px;
}
@@ -30,13 +30,13 @@
}
.viewMenu {
composes: menu from '~Components/Menu/Menu.css';
composes: menu from 'Components/Menu/Menu.css';
line-height: 31px;
}
.loading {
composes: loading from '~Components/Loading/LoadingIndicator.css';
composes: loading from 'Components/Loading/LoadingIndicator.css';
margin-top: 5px;
margin-right: 10px;

View File

@@ -83,7 +83,6 @@ class CalendarHeader extends Component {
end,
longDateFormat,
isSmallScreen,
collapseViewButtons,
onTodayPress,
onPreviousPress,
onNextPress
@@ -146,7 +145,7 @@ class CalendarHeader extends Component {
}
{
collapseViewButtons ?
isSmallScreen ?
<Menu
className={styles.viewMenu}
alignMenu={align.RIGHT}
@@ -159,18 +158,6 @@ class CalendarHeader extends Component {
</MenuButton>
<MenuContent>
{
isSmallScreen ?
null :
<ViewMenuItem
name={calendarViews.MONTH}
selectedView={view}
onPress={this.onViewChange}
>
Month
</ViewMenuItem>
}
<ViewMenuItem
name={calendarViews.WEEK}
selectedView={view}
@@ -256,7 +243,6 @@ CalendarHeader.propTypes = {
end: PropTypes.string.isRequired,
view: PropTypes.oneOf(calendarViews.all).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
collapseViewButtons: PropTypes.bool.isRequired,
longDateFormat: PropTypes.string.isRequired,
onViewChange: PropTypes.func.isRequired,
onTodayPress: PropTypes.func.isRequired,

View File

@@ -23,7 +23,6 @@ function createMapStateToProps() {
]);
result.isSmallScreen = dimensions.isSmallScreen;
result.collapseViewButtons = dimensions.isLargeScreen;
result.longDateFormat = uiSettings.longDateFormat;
return result;

View File

@@ -63,21 +63,6 @@ function Legend(props) {
/>
</div>
<div>
<LegendItem
status="onAir"
name="On Air"
tooltip="Episode is currently airing"
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="missing"
tooltip="Episode has aired and is missing from disk"
colorImpairedMode={colorImpairedMode}
/>
</div>
<div>
<LegendItem
status="downloading"

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './LegendIconItem.css';
import styles from './legendIconItem.css';
function LegendIconItem(props) {
const {

View File

@@ -13,29 +13,29 @@
*/
.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';
}
.unaired {
composes: unaired from '~Calendar/Events/CalendarEvent.css';
composes: unaired from 'Calendar/Events/CalendarEvent.css';
}

View File

@@ -56,7 +56,7 @@ class ErrorBoundary extends Component {
ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired,
errorComponent: PropTypes.elementType.isRequired
errorComponent: PropTypes.func.isRequired
};
export default ErrorBoundary;

View File

@@ -1,5 +1,5 @@
.modal {
composes: modal from '~Components/Modal/Modal.css';
composes: modal from 'Components/Modal/Modal.css';
height: 600px;
}

View File

@@ -1,12 +1,12 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
composes: modalBody from 'Components/Modal/ModalBody.css';
display: flex;
flex-direction: column;
}
.mappedDrivesWarning {
composes: alert from '~Components/Alert.css';
composes: alert from 'Components/Alert.css';
margin: 0;
margin-bottom: 20px;
@@ -18,7 +18,7 @@
}
.pathInput {
composes: inputWrapper from '~Components/Form/PathInput.css';
composes: pathInputWrapper from 'Components/Form/PathInput.css';
flex: 0 0 auto;
}

View File

@@ -144,7 +144,6 @@ class FileBrowserModalContent extends Component {
<Scroller
ref={this.setScrollerRef}
className={styles.scroller}
scrollDirection={scrollDirections.BOTH}
>
{
!!error &&
@@ -153,10 +152,7 @@ class FileBrowserModalContent extends Component {
{
isPopulated && !error &&
<Table
horizontalScroll={false}
columns={columns}
>
<Table columns={columns}>
<TableBody>
{
emptyParent &&

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